{"repo": "bgdnvk/clanker", "files": {".clanker.example.yaml": {"4d7af4d3f13f7651fb538328b6d57eacdfc4f8d7": {"t": 1785745198.2736197, "n": 11956, "h": "b053929c745b2825", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Sentry.io (for `clanker sentry ask ...` and `clanker sentry list issues`):\n# sentry:\n# auth_token: \"\" # Sentry User Auth Token (or set SENTRY_AUTH_TOKEN)\n# # Generate at https://sentry.io/settings/account/api/auth-tokens/\n# # Recommended scopes: org:read, project:read, project:write,\n# # event:read, event:admin, alerts:read, alerts:write\n# # project:write covers release management and alert rule mutations.\n# org_slug: \"\" # Sentry org slug (or set SENTRY_ORG)\n# default_project: \"\" # Optional default project slug for events/releases/alerts\n# # (or set SENTRY_PROJECT)\n# host: \"sentry.io\" # Override for self-hosted Sentry (or set SENTRY_HOST)\n# # Single-tenant EU customers use \".sentry.io\"\n\n# Linear (for `clanker linear ask ...` and `clanker linear list issues`):\n# linear:\n# api_key: \"\" # Linear Personal API Key (or set LINEAR_API_KEY)\n# # Generate at https://linear.app/settings/account/api\n# # IMPORTANT: the Authorization header is the raw key —\n# # do NOT add a \"Bearer \" prefix (Linear's #1 footgun).\n# workspace_id: \"\" # Workspace UUID (or set LINEAR_WORKSPACE_ID)\n# # Optional — only needed to scope conversation history;\n# # the API key already implies a single workspace.\n# default_team: \"\" # Default team key e.g. \"ENG\" (or set LINEAR_TEAM)\n# # Used by `clanker linear ask` to scope cycles + filters.\n#\n# Infra-resource annotation convention:\n# Tag Linear issues that reference a cloud resource with a label of the\n# form `infra::` (e.g. `infra:lambda:arn:aws:lambda:us-east-1:123:foo`).\n# The desktop app's resource drawer queries `/api/linear/issues/by-label/...`\n# to surface linked work on a resource — without the label there's nothing\n# to link. Create via `clanker linear label create infra:lambda:arn:... `.\n#\n# Conversation history: ~/.clanker/linear-{workspaceID}.json (or {team} when\n# workspace_id is empty). Delete to start fresh.\n\n# Notion (for `clanker notion ask ...` and `clanker notion search ...`):\n# notion:\n# integration_token: \"\" # Internal Integration token (or set NOTION_API_KEY)\n# # Create at https://www.notion.so/profile/integrations\n# # IMPORTANT: tokens start with ZERO access. You must\n# # explicitly share each page/database with the\n# # integration via \"...\" → \"Connections\" in Notion.\n# # This is Notion's #1 UX papercut — surface it when\n# # search returns empty.\n# # Auth header IS \"Authorization: Bearer \"\n# # (opposite of Linear — Notion does use the prefix).\n# default_database_id: \"\" # Optional default database UUID (or set NOTION_DATABASE_ID)\n# # Used by `clanker notion db row create --db `\n# # so you don't have to retype the id every time.\n#\n# Conversation history: ~/.clanker/notion-{workspaceName}.json. The workspace\n# name comes from `GET /v1/users/me` → `bot.workspace_name`. Delete to start\n# fresh.\n#\n# Rate limit: Notion enforces ~3 req/sec average per integration — tighter\n# than Linear's. The client backs off exponentially on 429 with the\n# Retry-After header, so heavy parallel ask flows degrade gracefully.\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Clanker SRE bot (for `clanker sre ...`):\n# sre:\n# enabled: true\n# cerebro_url: \"http://127.0.0.1:8080/api\" # Or set CLANKER_CEREBRO_URL\n# ingest_token: \"\" # Prefer CLANKER_CEREBRO_INGEST_TOKEN\n# interval: \"60s\"\n# target: \"docker\" # docker, local, launchd, systemd, k8s, cloud-vm\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "7e654fec011b871d3ab0490314b682701aa8e589": {"t": 1785748530.1094902, "n": 11956, "h": "b053929c745b2825", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Sentry.io (for `clanker sentry ask ...` and `clanker sentry list issues`):\n# sentry:\n# auth_token: \"\" # Sentry User Auth Token (or set SENTRY_AUTH_TOKEN)\n# # Generate at https://sentry.io/settings/account/api/auth-tokens/\n# # Recommended scopes: org:read, project:read, project:write,\n# # event:read, event:admin, alerts:read, alerts:write\n# # project:write covers release management and alert rule mutations.\n# org_slug: \"\" # Sentry org slug (or set SENTRY_ORG)\n# default_project: \"\" # Optional default project slug for events/releases/alerts\n# # (or set SENTRY_PROJECT)\n# host: \"sentry.io\" # Override for self-hosted Sentry (or set SENTRY_HOST)\n# # Single-tenant EU customers use \".sentry.io\"\n\n# Linear (for `clanker linear ask ...` and `clanker linear list issues`):\n# linear:\n# api_key: \"\" # Linear Personal API Key (or set LINEAR_API_KEY)\n# # Generate at https://linear.app/settings/account/api\n# # IMPORTANT: the Authorization header is the raw key —\n# # do NOT add a \"Bearer \" prefix (Linear's #1 footgun).\n# workspace_id: \"\" # Workspace UUID (or set LINEAR_WORKSPACE_ID)\n# # Optional — only needed to scope conversation history;\n# # the API key already implies a single workspace.\n# default_team: \"\" # Default team key e.g. \"ENG\" (or set LINEAR_TEAM)\n# # Used by `clanker linear ask` to scope cycles + filters.\n#\n# Infra-resource annotation convention:\n# Tag Linear issues that reference a cloud resource with a label of the\n# form `infra::` (e.g. `infra:lambda:arn:aws:lambda:us-east-1:123:foo`).\n# The desktop app's resource drawer queries `/api/linear/issues/by-label/...`\n# to surface linked work on a resource — without the label there's nothing\n# to link. Create via `clanker linear label create infra:lambda:arn:... `.\n#\n# Conversation history: ~/.clanker/linear-{workspaceID}.json (or {team} when\n# workspace_id is empty). Delete to start fresh.\n\n# Notion (for `clanker notion ask ...` and `clanker notion search ...`):\n# notion:\n# integration_token: \"\" # Internal Integration token (or set NOTION_API_KEY)\n# # Create at https://www.notion.so/profile/integrations\n# # IMPORTANT: tokens start with ZERO access. You must\n# # explicitly share each page/database with the\n# # integration via \"...\" → \"Connections\" in Notion.\n# # This is Notion's #1 UX papercut — surface it when\n# # search returns empty.\n# # Auth header IS \"Authorization: Bearer \"\n# # (opposite of Linear — Notion does use the prefix).\n# default_database_id: \"\" # Optional default database UUID (or set NOTION_DATABASE_ID)\n# # Used by `clanker notion db row create --db `\n# # so you don't have to retype the id every time.\n#\n# Conversation history: ~/.clanker/notion-{workspaceName}.json. The workspace\n# name comes from `GET /v1/users/me` → `bot.workspace_name`. Delete to start\n# fresh.\n#\n# Rate limit: Notion enforces ~3 req/sec average per integration — tighter\n# than Linear's. The client backs off exponentially on 429 with the\n# Retry-After header, so heavy parallel ask flows degrade gracefully.\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Clanker SRE bot (for `clanker sre ...`):\n# sre:\n# enabled: true\n# cerebro_url: \"http://127.0.0.1:8080/api\" # Or set CLANKER_CEREBRO_URL\n# ingest_token: \"\" # Prefer CLANKER_CEREBRO_INGEST_TOKEN\n# interval: \"60s\"\n# target: \"docker\" # docker, local, launchd, systemd, k8s, cloud-vm\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "825ffa5b3ecd7f7516425960e250fc39f74ef5ca": {"t": 1785748531.2325613, "n": 10545, "h": "6956d733977d8828", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Sentry.io (for `clanker sentry ask ...` and `clanker sentry list issues`):\n# sentry:\n# auth_token: \"\" # Sentry User Auth Token (or set SENTRY_AUTH_TOKEN)\n# # Generate at https://sentry.io/settings/account/api/auth-tokens/\n# # Recommended scopes: org:read, project:read, project:write,\n# # event:read, event:admin, alerts:read, alerts:write\n# # project:write covers release management and alert rule mutations.\n# org_slug: \"\" # Sentry org slug (or set SENTRY_ORG)\n# default_project: \"\" # Optional default project slug for events/releases/alerts\n# # (or set SENTRY_PROJECT)\n# host: \"sentry.io\" # Override for self-hosted Sentry (or set SENTRY_HOST)\n# # Single-tenant EU customers use \".sentry.io\"\n\n# Linear (for `clanker linear ask ...` and `clanker linear list issues`):\n# linear:\n# api_key: \"\" # Linear Personal API Key (or set LINEAR_API_KEY)\n# # Generate at https://linear.app/settings/account/api\n# # IMPORTANT: the Authorization header is the raw key —\n# # do NOT add a \"Bearer \" prefix (Linear's #1 footgun).\n# workspace_id: \"\" # Workspace UUID (or set LINEAR_WORKSPACE_ID)\n# # Optional — only needed to scope conversation history;\n# # the API key already implies a single workspace.\n# default_team: \"\" # Default team key e.g. \"ENG\" (or set LINEAR_TEAM)\n# # Used by `clanker linear ask` to scope cycles + filters.\n#\n# Infra-resource annotation convention:\n# Tag Linear issues that reference a cloud resource with a label of the\n# form `infra::` (e.g. `infra:lambda:arn:aws:lambda:us-east-1:123:foo`).\n# The desktop app's resource drawer queries `/api/linear/issues/by-label/...`\n# to surface linked work on a resource — without the label there's nothing\n# to link. Create via `clanker linear label create infra:lambda:arn:... `.\n#\n# Conversation history: ~/.clanker/linear-{workspaceID}.json (or {team} when\n# workspace_id is empty). Delete to start fresh.\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Clanker SRE bot (for `clanker sre ...`):\n# sre:\n# enabled: true\n# cerebro_url: \"http://127.0.0.1:8080/api\" # Or set CLANKER_CEREBRO_URL\n# ingest_token: \"\" # Prefer CLANKER_CEREBRO_INGEST_TOKEN\n# interval: \"60s\"\n# target: \"docker\" # docker, local, launchd, systemd, k8s, cloud-vm\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "b6407c8e4ce412c712fd78c4b9d43b738c2f4599": {"t": 1785748532.3425446, "n": 9170, "h": "f8b98df94d9a47c7", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Sentry.io (for `clanker sentry ask ...` and `clanker sentry list issues`):\n# sentry:\n# auth_token: \"\" # Sentry User Auth Token (or set SENTRY_AUTH_TOKEN)\n# # Generate at https://sentry.io/settings/account/api/auth-tokens/\n# # Recommended scopes: org:read, project:read, project:write,\n# # event:read, event:admin, alerts:read, alerts:write\n# # project:write covers release management and alert rule mutations.\n# org_slug: \"\" # Sentry org slug (or set SENTRY_ORG)\n# default_project: \"\" # Optional default project slug for events/releases/alerts\n# # (or set SENTRY_PROJECT)\n# host: \"sentry.io\" # Override for self-hosted Sentry (or set SENTRY_HOST)\n# # Single-tenant EU customers use \".sentry.io\"\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Clanker SRE bot (for `clanker sre ...`):\n# sre:\n# enabled: true\n# cerebro_url: \"http://127.0.0.1:8080/api\" # Or set CLANKER_CEREBRO_URL\n# ingest_token: \"\" # Prefer CLANKER_CEREBRO_INGEST_TOKEN\n# interval: \"60s\"\n# target: \"docker\" # docker, local, launchd, systemd, k8s, cloud-vm\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "31274b5e095383becc0a5b2ef9dc95de82e1bc8d": {"t": 1785748533.5695977, "n": 8243, "h": "52e53e21b7fb3552", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Clanker SRE bot (for `clanker sre ...`):\n# sre:\n# enabled: true\n# cerebro_url: \"http://127.0.0.1:8080/api\" # Or set CLANKER_CEREBRO_URL\n# ingest_token: \"\" # Prefer CLANKER_CEREBRO_INGEST_TOKEN\n# interval: \"60s\"\n# target: \"docker\" # docker, local, launchd, systemd, k8s, cloud-vm\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "ad31d1577e654b305eef9d0228695fba43568eec": {"t": 1785748534.6520143, "n": 7900, "h": "27d16ec7d05bd3e8", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Fly.io (for `clanker fly ...` and `clanker ask --flyio ...`):\n# flyio:\n# api_token: \"\" # Fly.io API token (or set FLY_API_TOKEN / FLY_ACCESS_TOKEN)\n# org_slug: \"\" # Optional org slug filter (or set FLY_ORG / FLY_ORG_SLUG).\n# # Empty = see resources across every org the token can access.\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "ee0e660e00f74cc580dafccb71ce8e88e0117951": {"t": 1785748535.7715569, "n": 7555, "h": "64997d7b9822f85b", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Verda Cloud (for `clanker verda ...` and `clanker ask --verda ...`):\n# verda:\n# client_id: \"\" # Verda OAuth2 client ID (or set VERDA_CLIENT_ID, or run `verda auth login`)\n# client_secret: \"\" # Verda OAuth2 client secret (or set VERDA_CLIENT_SECRET, or run `verda auth login`)\n# default_project_id: \"\" # Verda project ID (or set VERDA_PROJECT_ID)\n# default_location: \"\" # Default datacenter code, e.g. \"FIN-01\", \"ICE-01\"\n# default_ssh_key_id: \"\" # SSH key UUID used by k8s create / deploy flows\n# ssh_key_path: \"~/.ssh/id_ed25519\" # Local private key used to pull kubeconfig from Verda Instant Clusters\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "d508699ec423ffaa06377ad1fa3321e898a70fa8": {"t": 1785748537.0008976, "n": 6911, "h": "65c6ca5f353af442", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Vercel (for `clanker vercel ...` and `clanker ask --vercel ...`):\n# vercel:\n# api_token: \"\" # Vercel API token (or set VERCEL_TOKEN / VERCEL_API_TOKEN)\n# team_id: \"\" # Optional team ID for team-scoped accounts (or set VERCEL_TEAM_ID / VERCEL_ORG_ID)\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "cf4c8bef8b043dcb7cf98d4fa4968fc98679e536": {"t": 1785748538.0625532, "n": 6630, "h": "90d4356e5302f09a", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# Azure infrastructure and Azure DevOps context:\n# infra:\n# azure:\n# subscription_id: your-azure-subscription-id\n# devops:\n# organization: your-azure-devops-org # or full URL such as https://dev.azure.com/your-org\n# project: your-azure-devops-project\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Databases (read-only inspection only; clanker does not modify data):\n# databases:\n# default_connection: dev\n# connections:\n# dev:\n# driver: postgres\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "b1ebabc88fa8f0c7dd5050f8012214d104f5d3ca": {"t": 1785748539.1581092, "n": 6298, "h": "7a6be8df36aa1179", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n auth_method: \"\" # \"apiKey\" or \"oauth\" -- oauth uses ChatGPT account login\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "e41a40bad5024b1f410caeee5ca0b749a5ad3d86": {"t": 1785748540.5456293, "n": 6217, "h": "2cebcc52336eef28", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n # Optional for OpenAI-compatible local servers such as llama-server.\n # If the endpoint is localhost, api_key can stay empty.\n # local_model_inference_url: \"http://127.0.0.1:8080/v1\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n# # local_model_inference_url: \"http://127.0.0.1:8080/v1\" # optional for local model inference servers\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "a0e7079a445890e1d45c730791b4a1eebb977e18": {"t": 1785748541.8328383, "n": 5908, "h": "a17519187a94be27", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Hermes Agent (for `clanker talk` and `clanker ask --agent hermes`):\n# hermes:\n# path: \"./vendor/hermes-agent\" # Auto-detected if omitted\n# model: \"anthropic/claude-opus-4\" # Model via OpenRouter\n# base_url: \"https://openrouter.ai/api/v1\"\n# openrouter_api_key: \"\" # Or set OPENROUTER_API_KEY env var\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "350625344ff2b7a59f71acad6a6650acc9bb40c8": {"t": 1785748543.9269826, "n": 5584, "h": "da180bd14fbc8ac6", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # Cohere example:\n # cohere:\n # model: command-a-03-2025\n # api_key_env: COHERE_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Use Cohere:\n# ai:\n# default_provider: cohere\n# providers:\n# cohere:\n# model: command-a-03-2025\n# api_key_env: COHERE_API_KEY\n# # Pass at runtime: clanker ask --ai-profile cohere --cohere-key \"$COHERE_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "3d75c573a40f8657628f8f4f70c52aabe9a920e5": {"t": 1785748545.0121567, "n": 5240, "h": "eb4b02657562263d", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for `clanker hetzner ...` and `clanker ask --hetzner ...`):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "eaaa4c5b526ffc51d344739305a8fc74b7a401ad": {"t": 1785748546.4248574, "n": 5074, "h": "99bb0c4fa9c34d15", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Digital Ocean (for `clanker do ...` and `clanker ask --digitalocean ...`):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "f66a3aaf9dc87834ca5519d0cae4e1740bd936c1": {"t": 1785748547.4834583, "n": 4875, "h": "ca3fd84d0e7fbdff", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\n # DeepSeek example:\n # deepseek:\n # model: deepseek-chat\n # api_key_env: DEEPSEEK_API_KEY\n\n # MiniMax example:\n # minimax:\n # model: MiniMax-M2.5\n # api_key_env: MINIMAX_API_KEY\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Use DeepSeek:\n# ai:\n# default_provider: deepseek\n# providers:\n# deepseek:\n# model: deepseek-chat # or deepseek-reasoner\n# api_key_env: DEEPSEEK_API_KEY\n# # Pass at runtime: clanker ask --ai-profile deepseek --deepseek-key \"$DEEPSEEK_API_KEY\" ...\n\n# Use MiniMax:\n# ai:\n# default_provider: minimax\n# providers:\n# minimax:\n# model: MiniMax-M2.5 # or MiniMax-M2.5-highspeed, MiniMax-M2.1, MiniMax-M2.1-highspeed, MiniMax-M2\n# api_key_env: MINIMAX_API_KEY\n# # Pass at runtime: clanker ask --ai-profile minimax --minimax-key \"$MINIMAX_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "63b90b385a2b4064d38f7b53d9a2575c0c8f134d": {"t": 1785748548.6386912, "n": 4072, "h": "f04996e8a1e15879", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# # Default: testing\n# env: \"testing\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "0bf77586fd0dbc338b5ed3fcc2cb08b1026869b8": {"t": 1785748549.880838, "n": 4049, "h": "3ede4219453fd259", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# Backend integration (for storing and retrieving credentials across machines):\n# backend:\n# # API key for clanker backend authentication (or set CLANKER_BACKEND_API_KEY)\n# api_key: \"\"\n# # Backend environment: testing, staging, or production (or set CLANKER_BACKEND_ENV)\n# env: \"staging\"\n# # Custom backend URL, overrides env if set (or set CLANKER_BACKEND_URL)\n# # url: \"https://custom-url.example.com\"\n#\n# Use `clanker credentials store aws --profile ` to upload your AWS credentials.\n# Then use `clanker ask \"...\" --api-key ` to query using backend credentials.\n\n# General settings:\n# timeout: 30\n"}, "589ff74bb63b0848fca098f9e1038cee2c9c22d4": {"t": 1785748550.981938, "n": 3457, "h": "2079ddf01b96caba", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# Cloudflare (for `clanker cf ask ...` and `clanker ask --cloudflare ...`):\n# cloudflare:\n# api_token: \"\" # Cloudflare API token (or set CLOUDFLARE_API_TOKEN / CF_API_TOKEN)\n# account_id: \"\" # Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID)\n# default_zone: \"\" # Default zone name for DNS operations\n\n# General settings:\n# timeout: 30\n"}, "5721ae7b405dc40843feba1150775d28e3e2258c": {"t": 1785748552.2056615, "n": 3101, "h": "d562369e1b2b4b1c", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# Kubernetes (for `clanker k8s ask ...`):\n# kubernetes:\n# kubeconfig: \"\" # Path to kubeconfig (default: ~/.kube/config)\n# default_context: \"\" # Default kubectl context\n# default_namespace: \"\" # Default namespace (empty = all namespaces)\n# clusters:\n# production:\n# type: eks # eks or existing\n# name: prod-cluster # EKS cluster name\n# profile: prod-aws # AWS profile for EKS\n# region: us-east-1\n# staging:\n# type: eks\n# name: staging-cluster\n# profile: dev-aws\n# region: us-west-2\n# local:\n# type: existing # Use existing kubectl context\n# context: minikube\n\n# General settings:\n# timeout: 30\n"}, "906ee87b55a4c67b6b1e9c975c9d0e0098fe21fb": {"t": 1785748553.332253, "n": 2422, "h": "57373580c78a8a32", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n gcp:\n project_id: your-gcp-project-id\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# General settings:\n# timeout: 30\n"}, "cf4301ab6ac058596143849afa18decefbdc8119": {"t": 1785748554.7968645, "n": 2382, "h": "2ac3a4be691f109d", "c": "# Minimal Clanker Configuration\n# Copy this to ~/.clanker.yaml and fill in the placeholders.\n\nai:\n # Pick a provider and make sure its credentials exist.\n # If you omit this, clanker falls back to openai.\n default_provider: gemini-api\n\n providers:\n gemini-api:\n model: gemini-3-pro-preview\n api_key_env: GEMINI_API_KEY\n\n # OpenAI example:\n # Note: this CLI currently reads `ai.providers.openai.api_key` (not `api_key_env`).\n # You can either paste your key here or pass it at runtime with:\n # clanker ask --ai-profile openai --openai-key \"$OPENAI_API_KEY\" \"...\"\n openai:\n model: gpt-5\n api_key: \"\"\n\ninfra:\n default_provider: aws\n default_environment: dev\n\n aws:\n environments:\n dev:\n profile: your-aws-profile\n region: us-east-1\n\n\n# -------------------- Optional blocks --------------------\n\n# Use Bedrock (Claude) instead of Gemini API:\n# ai:\n# default_provider: bedrock\n# providers:\n# bedrock:\n# aws_profile: your-bedrock-aws-profile\n# region: us-east-1\n# model: us.anthropic.claude-sonnet-4-20250514-v1:0\n\n# Use OpenAI:\n# ai:\n# default_provider: openai\n# providers:\n# openai:\n# model: gpt-5\n# api_key: \"sk-...\" # or pass at runtime: clanker ask --openai-key \"$OPENAI_API_KEY\" ...\n\n# Multiple AWS environments (dev/stage/prod):\n# infra:\n# default_provider: aws\n# default_environment: dev\n# aws:\n# environments:\n# dev: { profile: your-dev-profile, region: us-east-1 }\n# stage: { profile: your-stage-profile, region: us-east-1 }\n# prod: { profile: your-prod-profile, region: us-east-1 }\n\n# GitHub context for `clanker ask --github ...`:\n# github:\n# token: \"ghp_...\" # optional for public repos\n# owner: your-org\n# repo: your-repo\n\n# AWS service keywords (used by internal routing). Note: this is under `aws.*`:\n# aws:\n# service_keywords:\n# api: [api, gateway]\n# serverless: [lambda, step-functions]\n\n# Postgres (for `clanker postgres ...`):\n# postgres:\n# default_connection: dev\n# connections:\n# dev:\n# host: localhost\n# port: 5432\n# database: your_db\n# username: postgres\n\n# Terraform (for `clanker terraform ...` and `clanker ask --terraform ...`):\n# terraform:\n# default_workspace: dev\n# workspaces:\n# dev:\n# path: /path/to/infra\n\n# General settings:\n# timeout: 30\n"}, "7511f1bf259d9cb5de5e025ddb247863b064de27": {"t": 1785748555.8974771, "n": 5171, "h": "d4b590bad9f340ea", "c": "# Example Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: bedrock # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n model: gemini-3-pro-preview\n \n gemini-api:\n model: gemini-3-pro-preview\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n \n gcp:\n environments:\n dev:\n project_id: your-dev-project\n region: us-central1\n description: Development GCP project\n stage:\n project_id: your-stage-project\n region: us-central1\n description: Staging GCP project\n prod:\n project_id: your-prod-project \n region: us-central1\n description: Production GCP project\n \n azure:\n environments:\n dev:\n subscription_id: dev-subscription-id\n resource_group: dev-rg\n region: eastus\n description: Development Azure subscription\n stage:\n subscription_id: stage-subscription-id\n resource_group: stage-rg\n region: eastus\n description: Staging Azure subscription\n prod:\n subscription_id: prod-subscription-id\n resource_group: prod-rg\n region: eastus\n description: Production Azure subscription\n\n# Service keywords for log analysis and service identification\nservice_keywords:\n api:\n - api\n - gateway\n - rest\n - graphql\n - endpoint\n - service\n web:\n - web\n - frontend\n - ui\n - webapp\n - dashboard\n auth:\n - auth\n - authentication\n - authorization\n - login\n - cognito\n - oauth\n - jwt\n - token\n database:\n - database\n - db\n - rds\n - postgres\n - mysql\n - mongodb\n - dynamodb\n - redis\n storage:\n - storage\n - s3\n - bucket\n - file\n - blob\n - object\n compute:\n - compute\n - ec2\n - lambda\n - function\n - container\n - ecs\n - fargate\n networking:\n - network\n - vpc\n - subnet\n - route\n - load-balancer\n - alb\n - nlb\n - cloudfront\n - cdn\n monitoring:\n - monitor\n - logs\n - metrics\n - cloudwatch\n - alerts\n - dashboard\n security:\n - security\n - waf\n - shield\n - firewall\n - encryption\n - kms\n - secrets\n cicd:\n - ci\n - cd\n - pipeline\n - build\n - deploy\n - github-actions\n - codebuild\n - codepipeline\n ml:\n - ml\n - ai\n - machine-learning\n - bedrock\n - sagemaker\n - model\n - inference\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\nverbose: false\ntimeout: 30 # Timeout for AI requests in seconds\n"}, "de3f784bebb00e38c5b7aeba1c9349b1fd21674b": {"t": 1785748556.9440477, "n": 5127, "h": "d7baeb8af350a2ac", "c": "# Example Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: bedrock # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-pro\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n \n gcp:\n environments:\n dev:\n project_id: your-dev-project\n region: us-central1\n description: Development GCP project\n stage:\n project_id: your-stage-project\n region: us-central1\n description: Staging GCP project\n prod:\n project_id: your-prod-project \n region: us-central1\n description: Production GCP project\n \n azure:\n environments:\n dev:\n subscription_id: dev-subscription-id\n resource_group: dev-rg\n region: eastus\n description: Development Azure subscription\n stage:\n subscription_id: stage-subscription-id\n resource_group: stage-rg\n region: eastus\n description: Staging Azure subscription\n prod:\n subscription_id: prod-subscription-id\n resource_group: prod-rg\n region: eastus\n description: Production Azure subscription\n\n# Service keywords for log analysis and service identification\nservice_keywords:\n api:\n - api\n - gateway\n - rest\n - graphql\n - endpoint\n - service\n web:\n - web\n - frontend\n - ui\n - webapp\n - dashboard\n auth:\n - auth\n - authentication\n - authorization\n - login\n - cognito\n - oauth\n - jwt\n - token\n database:\n - database\n - db\n - rds\n - postgres\n - mysql\n - mongodb\n - dynamodb\n - redis\n storage:\n - storage\n - s3\n - bucket\n - file\n - blob\n - object\n compute:\n - compute\n - ec2\n - lambda\n - function\n - container\n - ecs\n - fargate\n networking:\n - network\n - vpc\n - subnet\n - route\n - load-balancer\n - alb\n - nlb\n - cloudfront\n - cdn\n monitoring:\n - monitor\n - logs\n - metrics\n - cloudwatch\n - alerts\n - dashboard\n security:\n - security\n - waf\n - shield\n - firewall\n - encryption\n - kms\n - secrets\n cicd:\n - ci\n - cd\n - pipeline\n - build\n - deploy\n - github-actions\n - codebuild\n - codepipeline\n ml:\n - ml\n - ai\n - machine-learning\n - bedrock\n - sagemaker\n - model\n - inference\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\nverbose: false\ntimeout: 30 # Timeout for AI requests in seconds\n"}}, "cmd/config.go": {"4d7af4d3f13f7651fb538328b6d57eacdfc4f8d7": {"t": 1785745618.4843333, "n": 27536, "h": "9df997219532c2cd", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n # local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n clanker-cloud:\n model: gemini-3.5-flash\n base_url: https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\n api_key_env: CLANKER_CLOUD_AUTH_TOKEN\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Oracle Cloud Infrastructure (for 'clanker oracle ...' and 'clanker ask --oracle ...'):\n# oracle:\n# profile: DEFAULT # OCI CLI profile (or set OCI_CLI_PROFILE)\n# tenancy_ocid: \"\" # Root tenancy OCID for compartment discovery (or set OCI_TENANCY_OCID)\n# compartment_id: \"\" # Optional target compartment OCID (or set OCI_COMPARTMENT_ID)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = writePrivateUserConfig(configPath, []byte(defaultConfig))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(redactConfigForDisplay(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "1986894da2e62a39c129e5fc0d954c8666020e05": {"t": 1785748529.4182165, "n": 27536, "h": "9df997219532c2cd", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n # local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n clanker-cloud:\n model: gemini-3.5-flash\n base_url: https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\n api_key_env: CLANKER_CLOUD_AUTH_TOKEN\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# Oracle Cloud Infrastructure (for 'clanker oracle ...' and 'clanker ask --oracle ...'):\n# oracle:\n# profile: DEFAULT # OCI CLI profile (or set OCI_CLI_PROFILE)\n# tenancy_ocid: \"\" # Root tenancy OCID for compartment discovery (or set OCI_TENANCY_OCID)\n# compartment_id: \"\" # Optional target compartment OCID (or set OCI_COMPARTMENT_ID)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = writePrivateUserConfig(configPath, []byte(defaultConfig))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(redactConfigForDisplay(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "7c0b7fd7b1497a34150bfb2ac88b9085d608bb64": {"t": 1785748531.0802724, "n": 27174, "h": "cf9f6c7828dba76f", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n # local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n clanker-cloud:\n model: gemini-3.5-flash\n base_url: https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\n api_key_env: CLANKER_CLOUD_AUTH_TOKEN\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = writePrivateUserConfig(configPath, []byte(defaultConfig))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(redactConfigForDisplay(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "8246bb67c62106e2b09aa1bfacfdddbef9cb2a2f": {"t": 1785748532.3363652, "n": 27154, "h": "20150cdfd425e53f", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n # local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n clanker-cloud:\n model: gemini-3.5-flash\n base_url: https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\n api_key_env: CLANKER_CLOUD_AUTH_TOKEN\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "f333b3c79c0f63a29490d3c4c4c61cf991be8769": {"t": 1785748533.5859604, "n": 27007, "h": "23ad192349db58d4", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n\t\t\t# local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = writePrivateUserConfig(configPath, []byte(defaultConfig))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(redactConfigForDisplay(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "b9fe5c3c036470bcb5b80e9e44d450055edb0aba": {"t": 1785748534.8282819, "n": 26987, "h": "cf355281c95b7ea5", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tupdateChannel, _ := cmd.Flags().GetString(\"update-channel\")\n\t\tnormalizedUpdateChannel, err := updater.NormalizeChannel(updateChannel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n\t\t\t# local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n\n# Self-update settings for 'clanker update'.\n# Use \"release\" for the latest GitHub release, or \"main\" for the latest commit on the default branch.\nupdate:\n channel: __UPDATE_CHANNEL__\n`\n\t\tdefaultConfig = strings.ReplaceAll(defaultConfig, \"__UPDATE_CHANNEL__\", normalizedUpdateChannel)\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigInitCmd.Flags().String(\"update-channel\", updater.ChannelRelease, \"default self-update channel for clanker update: release or main\")\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "cf4c8bef8b043dcb7cf98d4fa4968fc98679e536": {"t": 1785748536.075697, "n": 26344, "h": "d146c66ba3197c12", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n\t\t\t# local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\n\tazure:\n\t\tsubscription_id: your-azure-subscription-id\n\t\tdevops:\n\t\t\torganization: your-azure-devops-org\n\t\t\tproject: your-azure-devops-project\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\ndatabases:\n\tdefault_connection: dev # Default database connection\n\t# Inspection is read-only. Clanker only opens database metadata sessions for SELECT and schema discovery.\n\tconnections:\n\t\tdev:\n\t\t\tdriver: postgres\n\t\t\thost: localhost\n\t\t\tport: 5432\n\t\t\tdatabase: your_dev_db\n\t\t\tusername: postgres\n\t\t\tdescription: Local PostgreSQL database\n\t\tsupabase:\n\t\t\tvendor: supabase\n\t\t\thost: your-project.supabase.co\n\t\t\tport: 5432\n\t\t\tdatabase: postgres\n\t\t\tusername: postgres\n\t\t\tpassword_env: SUPABASE_DB_PASSWORD\n\t\t\tpool_mode: session\n\t\t\tdescription: Supabase PostgreSQL database\n\t\tneon:\n\t\t\tvendor: neon\n\t\t\thost: ep-example.us-east-1.aws.neon.tech\n\t\t\tport: 5432\n\t\t\tdatabase: neondb\n\t\t\tusername: neon_user\n\t\t\tpassword_env: NEON_DB_PASSWORD\n\t\t\tsslmode: verify-full\n\t\t\tdescription: Neon PostgreSQL database\n\t\tmysql:\n\t\t\tdriver: mysql\n\t\t\thost: mysql.example.com\n\t\t\tport: 3306\n\t\t\tdatabase: app\n\t\t\tusername: app_user\n\t\t\tpassword_env: MYSQL_DB_PASSWORD\n\t\t\tdescription: MySQL database\n\t\tsqlite:\n\t\t\tdriver: sqlite\n\t\t\tpath: ./local-dev.sqlite\n\t\t\tdescription: Local SQLite database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "e41a40bad5024b1f410caeee5ca0b749a5ad3d86": {"t": 1785748537.3124795, "n": 25567, "h": "85b4f81f40566fb2", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n\t\t\t# local_model_inference_url: http://127.0.0.1:8080/v1\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "c50ad2b9c2b159198f4c076adf75c5d0a6ea765b": {"t": 1785748538.5236037, "n": 25510, "h": "f2a405ca9bcedfea", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: anthropic.claude-opus-4-6-v1\n region: us-west-1\n\n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n\n anthropic:\n model: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n\n gemini:\n project_id: your-gcp-project-id\n\n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n github-models:\n model: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n cohere:\n model: command-a-03-2025\n api_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "b033083f56b59465b0c5711de178f071c527e100": {"t": 1785748540.477459, "n": 25507, "h": "ff3476b9862406ae", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n\t\t\tmodel: anthropic.claude-opus-4-6-v1\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n\t\t\tmodel: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n\t\tgithub-models:\n\t\t\tmodel: openai/gpt-5.4\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n\t\tcohere:\n\t\t\tmodel: command-a-03-2025\n\t\t\tapi_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "350625344ff2b7a59f71acad6a6650acc9bb40c8": {"t": 1785748542.0389342, "n": 25464, "h": "ed5ad7db57acd57a", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n\t\t\tmodel: anthropic.claude-opus-4-6-v1\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n\t\t\tmodel: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n\t\tcohere:\n\t\t\tmodel: command-a-03-2025\n\t\t\tapi_key_env: COHERE_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tCohere LLMKeyStatus `json:\"cohere\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tfmt.Printf(\" DeepSeek: %v\\n\", result.LLM.DeepSeek.HasKey)\n\tfmt.Printf(\" Cohere: %v\\n\", result.LLM.Cohere.HasKey)\n\tfmt.Printf(\" MiniMax: %v\\n\", result.LLM.MiniMax.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tCohere: LLMKeyStatus{HasKey: os.Getenv(\"COHERE_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "3d75c573a40f8657628f8f4f70c52aabe9a920e5": {"t": 1785748543.4654245, "n": 25102, "h": "b96943e51f5d6b91", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n\t\t\tmodel: anthropic.claude-opus-4-6-v1\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n\t\t\tmodel: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# Hetzner Cloud (for 'clanker hetzner ...' and 'clanker ask --hetzner ...'):\n# hetzner:\n# api_token: \"\" # Hetzner Cloud API token (or set HCLOUD_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tHetzner HetznerCredentialsScan `json:\"hetzner\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// HetznerCredentialsScan holds detected Hetzner Cloud credentials\ntype HetznerCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tHetzner: scanHetznerCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// Hetzner\n\tfmt.Println(\"Hetzner Cloud:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Hetzner.HasToken)\n\tfmt.Printf(\" hcloud CLI: %v\\n\", result.Hetzner.CLIAvailable)\n\tif result.Hetzner.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Hetzner.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanHetznerCredentials() HetznerCredentialsScan {\n\tresult := HetznerCredentialsScan{\n\t\tHasToken: os.Getenv(\"HCLOUD_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"hcloud\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "eaaa4c5b526ffc51d344739305a8fc74b7a401ad": {"t": 1785748544.7040672, "n": 24085, "h": "29ec27d2c50c8cf9", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n\t\t\tmodel: anthropic.claude-opus-4-6-v1\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n\t\t\tmodel: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# Digital Ocean (for 'clanker do ...' and 'clanker ask --digitalocean ...'):\n# digitalocean:\n# api_token: \"\" # Digital Ocean API token (or set DO_API_TOKEN / DIGITALOCEAN_ACCESS_TOKEN)\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tDigitalOcean DigitalOceanCredentialsScan `json:\"digitalocean\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// DigitalOceanCredentialsScan holds detected Digital Ocean credentials\ntype DigitalOceanCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tDigitalOcean: scanDigitalOceanCredentials(),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// Digital Ocean\n\tfmt.Println(\"Digital Ocean:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.DigitalOcean.HasToken)\n\tfmt.Printf(\" doctl CLI: %v\\n\", result.DigitalOcean.CLIAvailable)\n\tif result.DigitalOcean.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.DigitalOcean.Error)\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanDigitalOceanCredentials() DigitalOceanCredentialsScan {\n\tresult := DigitalOceanCredentialsScan{\n\t\tHasToken: os.Getenv(\"DO_API_TOKEN\") != \"\" || os.Getenv(\"DIGITALOCEAN_ACCESS_TOKEN\") != \"\",\n\t}\n\n\tif _, err := exec.LookPath(\"doctl\"); err == nil {\n\t\tresult.CLIAvailable = true\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "3cfcea7871e661f3b8cbf07cf90bdfb1da91ab4a": {"t": 1785748545.929968, "n": 22898, "h": "d8bfad48e0e642fa", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n\t\t\tmodel: anthropic.claude-opus-4-6-v1\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n\t\t\tmodel: claude-opus-4-6\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "f66a3aaf9dc87834ca5519d0cae4e1740bd936c1": {"t": 1785748547.5095417, "n": 22927, "h": "911b52303e77570e", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n deepseek:\n model: deepseek-chat\n api_key_env: DEEPSEEK_API_KEY\n\n minimax:\n model: MiniMax-M2.5\n api_key_env: MINIMAX_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tDeepSeek LLMKeyStatus `json:\"deepseek\"`\n\tMiniMax LLMKeyStatus `json:\"minimax\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tDeepSeek: LLMKeyStatus{HasKey: os.Getenv(\"DEEPSEEK_API_KEY\") != \"\"},\n\t\tMiniMax: LLMKeyStatus{HasKey: os.Getenv(\"MINIMAX_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "6778be0fb3b7102bee24d2bffba3a96aff5828e5": {"t": 1785748548.7256289, "n": 22532, "h": "0c926336bb01bfe1", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "824df2db883d17a03267e2fca926cd8a702ad967": {"t": 1785748550.939619, "n": 22538, "h": "c2434f3d451509b4", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nYou can specify custom file paths and environment variable keys to scan\nin addition to the default locations.\n\nExamples:\n clanker config scan\n clanker config scan --output json\n clanker config scan --aws-paths ~/.custom/aws-creds,~/.another/credentials\n clanker config scan --gcp-paths ~/.config/gcloud/custom-creds.json\n clanker config scan --llm-env MY_OPENAI_KEY,MY_ANTHROPIC_KEY`,\n\tRunE: runConfigScan,\n}\n\n// CustomScanConfig holds custom paths and env keys for scanning\ntype CustomScanConfig struct {\n\tAWSPaths []string\n\tGCPPaths []string\n\tCloudflareEnv []string\n\tLLMEnv []string\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCustomPaths []string `json:\"customPaths,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n\tCustomEnvKeys []string `json:\"customEnvKeys,omitempty\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\tawsPaths, _ := cmd.Flags().GetStringSlice(\"aws-paths\")\n\tgcpPaths, _ := cmd.Flags().GetStringSlice(\"gcp-paths\")\n\tcloudflareEnv, _ := cmd.Flags().GetStringSlice(\"cloudflare-env\")\n\tllmEnv, _ := cmd.Flags().GetStringSlice(\"llm-env\")\n\n\tcustomConfig := CustomScanConfig{\n\t\tAWSPaths: awsPaths,\n\t\tGCPPaths: gcpPaths,\n\t\tCloudflareEnv: cloudflareEnv,\n\t\tLLMEnv: llmEnv,\n\t}\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(customConfig),\n\t\tGCP: scanGCPCredentials(customConfig),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(customConfig),\n\t\tLLM: scanLLMKeys(customConfig),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tif len(result.GCP.CustomPaths) > 0 {\n\t\tfmt.Println(\" Custom credential files found:\")\n\t\tfor _, p := range result.GCP.CustomPaths {\n\t\t\tfmt.Printf(\" - %s\\n\", p)\n\t\t}\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tif len(result.Cloudflare.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.Cloudflare.CustomEnvKeys, \", \"))\n\t}\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n\tif len(result.LLM.CustomEnvKeys) > 0 {\n\t\tfmt.Printf(\" Custom env keys found: %s\\n\", strings.Join(result.LLM.CustomEnvKeys, \", \"))\n\t}\n}\n\nfunc scanAWSProfiles(customConfig CustomScanConfig) AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default paths\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Scan custom AWS paths\n\tfor _, customPath := range customConfig.AWSPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tcustomProfiles := parseAWSINIFile(expandedPath, \"custom:\"+customPath)\n\t\tfor _, p := range customProfiles {\n\t\t\tif _, exists := profileMap[p.Name]; !exists {\n\t\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\t\tName: p.Name,\n\t\t\t\t\tRegion: p.Region,\n\t\t\t\t\tSource: p.Source,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\n// expandTilde expands ~ to home directory in paths\nfunc expandTilde(path string, home string) string {\n\tif strings.HasPrefix(path, \"~/\") {\n\t\treturn filepath.Join(home, path[2:])\n\t}\n\tif path == \"~\" {\n\t\treturn home\n\t}\n\treturn path\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials(customConfig CustomScanConfig) GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t\tCustomPaths: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\t// Default ADC path\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\t// Check custom GCP paths\n\tfor _, customPath := range customConfig.GCPPaths {\n\t\texpandedPath := expandTilde(customPath, home)\n\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\tresult.CustomPaths = append(result.CustomPaths, expandedPath)\n\t\t}\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials(customConfig CustomScanConfig) CloudflareCredentialsScan {\n\tresult := CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom env keys for Cloudflare\n\tfor _, envKey := range customConfig.CloudflareEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc scanLLMKeys(customConfig CustomScanConfig) LLMCredentialsScan {\n\tresult := LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t\tCustomEnvKeys: []string{},\n\t}\n\n\t// Check custom LLM env keys\n\tfor _, envKey := range customConfig.LLMEnv {\n\t\tif os.Getenv(envKey) != \"\" {\n\t\t\tresult.CustomEnvKeys = append(result.CustomEnvKeys, envKey)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n\tconfigScanCmd.Flags().StringSlice(\"aws-paths\", []string{}, \"Custom AWS credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"gcp-paths\", []string{}, \"Custom GCP credential file paths to scan (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"cloudflare-env\", []string{}, \"Custom Cloudflare environment variable keys to check (comma-separated)\")\n\tconfigScanCmd.Flags().StringSlice(\"llm-env\", []string{}, \"Custom LLM API key environment variables to check (comma-separated)\")\n}\n"}, "3707170184aa87b1b97f885c3f879fafa6ef2759": {"t": 1785748552.212177, "n": 18864, "h": "669361de764a2b9d", "c": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nvar configScanCmd = &cobra.Command{\n\tUse: \"scan\",\n\tShort: \"Scan system for available credentials\",\n\tLong: `Detect AWS profiles, GCP projects, Azure subscriptions, Cloudflare, and LLM API keys.\n\nThis command scans the local system for available cloud provider credentials\nand API keys that can be used with clanker.\n\nExamples:\n clanker config scan\n clanker config scan --output json`,\n\tRunE: runConfigScan,\n}\n\n// ScanResult holds all detected credentials\ntype ScanResult struct {\n\tAWS AWSCredentialsScan `json:\"aws\"`\n\tGCP GCPCredentialsScan `json:\"gcp\"`\n\tAzure AzureCredentialsScan `json:\"azure\"`\n\tCloudflare CloudflareCredentialsScan `json:\"cloudflare\"`\n\tLLM LLMCredentialsScan `json:\"llm\"`\n}\n\n// AWSCredentialsScan holds detected AWS profiles\ntype AWSCredentialsScan struct {\n\tProfiles []AWSProfileInfo `json:\"profiles\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AWSProfileInfo holds info about a single AWS profile\ntype AWSProfileInfo struct {\n\tName string `json:\"name\"`\n\tRegion string `json:\"region,omitempty\"`\n\tSource string `json:\"source\"`\n}\n\n// GCPCredentialsScan holds detected GCP credentials\ntype GCPCredentialsScan struct {\n\tHasADC bool `json:\"hasADC\"`\n\tADCPath string `json:\"adcPath,omitempty\"`\n\tProjects []string `json:\"projects,omitempty\"`\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureCredentialsScan holds detected Azure subscriptions\ntype AzureCredentialsScan struct {\n\tCLIAvailable bool `json:\"cliAvailable\"`\n\tSubscriptions []AzureSubscriptionInfo `json:\"subscriptions,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// AzureSubscriptionInfo holds info about an Azure subscription\ntype AzureSubscriptionInfo struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state,omitempty\"`\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\n// CloudflareCredentialsScan holds detected Cloudflare credentials\ntype CloudflareCredentialsScan struct {\n\tHasToken bool `json:\"hasToken\"`\n\tHasAccountID bool `json:\"hasAccountId\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// LLMCredentialsScan holds detected LLM API keys\ntype LLMCredentialsScan struct {\n\tOpenAI LLMKeyStatus `json:\"openai\"`\n\tAnthropic LLMKeyStatus `json:\"anthropic\"`\n\tGemini LLMKeyStatus `json:\"gemini\"`\n}\n\n// LLMKeyStatus indicates whether an LLM key was detected\ntype LLMKeyStatus struct {\n\tHasKey bool `json:\"hasKey\"`\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc runConfigScan(cmd *cobra.Command, args []string) error {\n\toutputFormat, _ := cmd.Flags().GetString(\"output\")\n\n\tresult := ScanResult{\n\t\tAWS: scanAWSProfiles(),\n\t\tGCP: scanGCPCredentials(),\n\t\tAzure: scanAzureSubscriptions(),\n\t\tCloudflare: scanCloudflareCredentials(),\n\t\tLLM: scanLLMKeys(),\n\t}\n\n\tif outputFormat == \"json\" {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\t// Pretty print for human consumption\n\tprintScanResult(result)\n\treturn nil\n}\n\nfunc printScanResult(result ScanResult) {\n\tfmt.Println(\"=== System Credentials Scan ===\")\n\tfmt.Println()\n\n\t// AWS\n\tfmt.Println(\"AWS Profiles:\")\n\tif len(result.AWS.Profiles) == 0 {\n\t\tfmt.Println(\" No profiles detected\")\n\t} else {\n\t\tfor _, p := range result.AWS.Profiles {\n\t\t\tregion := p.Region\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"(no region)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s [%s] (%s)\\n\", p.Name, region, p.Source)\n\t\t}\n\t}\n\tif result.AWS.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.AWS.Error)\n\t}\n\tfmt.Println()\n\n\t// GCP\n\tfmt.Println(\"GCP:\")\n\tif result.GCP.HasADC {\n\t\tfmt.Printf(\" Application Default Credentials: Found at %s\\n\", result.GCP.ADCPath)\n\t} else {\n\t\tfmt.Println(\" Application Default Credentials: Not found\")\n\t}\n\tfmt.Printf(\" gcloud CLI: %v\\n\", result.GCP.CLIAvailable)\n\tif len(result.GCP.Projects) > 0 {\n\t\tfmt.Printf(\" Projects: %s\\n\", strings.Join(result.GCP.Projects, \", \"))\n\t}\n\tif result.GCP.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.GCP.Error)\n\t}\n\tfmt.Println()\n\n\t// Azure\n\tfmt.Println(\"Azure:\")\n\tfmt.Printf(\" az CLI: %v\\n\", result.Azure.CLIAvailable)\n\tif len(result.Azure.Subscriptions) == 0 {\n\t\tfmt.Println(\" Subscriptions: None detected\")\n\t} else {\n\t\tfmt.Println(\" Subscriptions:\")\n\t\tfor _, s := range result.Azure.Subscriptions {\n\t\t\tdefaultMark := \"\"\n\t\t\tif s.IsDefault {\n\t\t\t\tdefaultMark = \" (default)\"\n\t\t\t}\n\t\t\tfmt.Printf(\" - %s (%s)%s\\n\", s.Name, s.ID, defaultMark)\n\t\t}\n\t}\n\tif result.Azure.Error != \"\" {\n\t\tfmt.Printf(\" Error: %s\\n\", result.Azure.Error)\n\t}\n\tfmt.Println()\n\n\t// Cloudflare\n\tfmt.Println(\"Cloudflare:\")\n\tfmt.Printf(\" API Token (env): %v\\n\", result.Cloudflare.HasToken)\n\tfmt.Printf(\" Account ID (env): %v\\n\", result.Cloudflare.HasAccountID)\n\tfmt.Println()\n\n\t// LLM Keys\n\tfmt.Println(\"LLM API Keys (from environment):\")\n\tfmt.Printf(\" OpenAI: %v\\n\", result.LLM.OpenAI.HasKey)\n\tfmt.Printf(\" Anthropic: %v\\n\", result.LLM.Anthropic.HasKey)\n\tfmt.Printf(\" Gemini: %v\\n\", result.LLM.Gemini.HasKey)\n}\n\nfunc scanAWSProfiles() AWSCredentialsScan {\n\tresult := AWSCredentialsScan{\n\t\tProfiles: []AWSProfileInfo{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\tcredPath := filepath.Join(home, \".aws\", \"credentials\")\n\tconfigPath := filepath.Join(home, \".aws\", \"config\")\n\n\tcredProfiles := parseAWSINIFile(credPath, \"credentials\")\n\tconfigProfiles := parseAWSINIFile(configPath, \"config\")\n\n\tprofileMap := make(map[string]*AWSProfileInfo)\n\n\tfor _, p := range credProfiles {\n\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\tName: p.Name,\n\t\t\tRegion: p.Region,\n\t\t\tSource: p.Source,\n\t\t}\n\t}\n\n\tfor _, p := range configProfiles {\n\t\tif existing, ok := profileMap[p.Name]; ok {\n\t\t\tif existing.Region == \"\" && p.Region != \"\" {\n\t\t\t\texisting.Region = p.Region\n\t\t\t}\n\t\t} else {\n\t\t\tprofileMap[p.Name] = &AWSProfileInfo{\n\t\t\t\tName: p.Name,\n\t\t\t\tRegion: p.Region,\n\t\t\t\tSource: p.Source,\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range profileMap {\n\t\tresult.Profiles = append(result.Profiles, *p)\n\t}\n\n\treturn result\n}\n\nfunc parseAWSINIFile(path string, source string) []AWSProfileInfo {\n\tprofiles := []AWSProfileInfo{}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn profiles\n\t}\n\tdefer file.Close()\n\n\tsectionPattern := regexp.MustCompile(`^\\s*\\[([^\\]]+)\\]\\s*$`)\n\tkvPattern := regexp.MustCompile(`^\\s*([^=\\s]+)\\s*=\\s*(.+?)\\s*$`)\n\n\tvar currentProfile *AWSProfileInfo\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif matches := sectionPattern.FindStringSubmatch(line); len(matches) == 2 {\n\t\t\tif currentProfile != nil {\n\t\t\t\tprofiles = append(profiles, *currentProfile)\n\t\t\t}\n\n\t\t\tsectionName := strings.TrimSpace(matches[1])\n\t\t\tprofileName := sectionName\n\n\t\t\tif source == \"config\" && strings.HasPrefix(sectionName, \"profile \") {\n\t\t\t\tprofileName = strings.TrimPrefix(sectionName, \"profile \")\n\t\t\t}\n\n\t\t\tcurrentProfile = &AWSProfileInfo{\n\t\t\t\tName: profileName,\n\t\t\t\tSource: source,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentProfile != nil {\n\t\t\tif matches := kvPattern.FindStringSubmatch(line); len(matches) == 3 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(matches[1]))\n\t\t\t\tvalue := strings.TrimSpace(matches[2])\n\n\t\t\t\tif key == \"region\" {\n\t\t\t\t\tcurrentProfile.Region = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif currentProfile != nil {\n\t\tprofiles = append(profiles, *currentProfile)\n\t}\n\n\treturn profiles\n}\n\nfunc scanGCPCredentials() GCPCredentialsScan {\n\tresult := GCPCredentialsScan{\n\t\tProjects: []string{},\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tresult.Error = \"could not determine home directory\"\n\t\treturn result\n\t}\n\n\tadcPath := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\tif _, err := os.Stat(adcPath); err == nil {\n\t\tresult.HasADC = true\n\t\tresult.ADCPath = adcPath\n\t}\n\n\tgcloudPath, err := findGcloudBinary()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, gcloudPath, \"config\", \"get-value\", \"project\")\n\toutput, err := cmd.Output()\n\tif err == nil {\n\t\tproject := strings.TrimSpace(string(output))\n\t\tif project != \"\" && project != \"(unset)\" {\n\t\t\tresult.Projects = append(result.Projects, project)\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, gcloudPath, \"config\", \"configurations\", \"list\", \"--format=json\")\n\toutput, err = cmd.Output()\n\tif err == nil {\n\t\tvar configs []struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tIsActive bool `json:\"is_active\"`\n\t\t\tProperties struct {\n\t\t\t\tCore struct {\n\t\t\t\t\tProject string `json:\"project\"`\n\t\t\t\t} `json:\"core\"`\n\t\t\t} `json:\"properties\"`\n\t\t}\n\t\tif json.Unmarshal(output, &configs) == nil {\n\t\t\tfor _, cfg := range configs {\n\t\t\t\tif cfg.Properties.Core.Project != \"\" {\n\t\t\t\t\tfound := false\n\t\t\t\t\tfor _, p := range result.Projects {\n\t\t\t\t\t\tif p == cfg.Properties.Core.Project {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !found {\n\t\t\t\t\t\tresult.Projects = append(result.Projects, cfg.Properties.Core.Project)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc findGcloudBinary() (string, error) {\n\tnames := []string{\"gcloud\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"gcloud.cmd\", \"gcloud.exe\", \"gcloud\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/gcloud\",\n\t\t\t\"/usr/local/bin/gcloud\",\n\t\t\t\"/snap/bin/gcloud\",\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"google-cloud-sdk\", \"bin\", \"gcloud\"))\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Google\", \"Cloud SDK\", \"google-cloud-sdk\", \"bin\", \"gcloud.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanAzureSubscriptions() AzureCredentialsScan {\n\tresult := AzureCredentialsScan{\n\t\tSubscriptions: []AzureSubscriptionInfo{},\n\t}\n\n\tazPath, err := findAzureCLI()\n\tif err != nil {\n\t\tresult.CLIAvailable = false\n\t\treturn result\n\t}\n\tresult.CLIAvailable = true\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, azPath, \"account\", \"list\", \"--output\", \"json\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tresult.Error = \"failed to list subscriptions (may need az login)\"\n\t\treturn result\n\t}\n\n\tvar subs []struct {\n\t\tID string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t\tState string `json:\"state\"`\n\t\tIsDefault bool `json:\"isDefault\"`\n\t}\n\n\tif json.Unmarshal(output, &subs) != nil {\n\t\tresult.Error = \"failed to parse subscription list\"\n\t\treturn result\n\t}\n\n\tfor _, sub := range subs {\n\t\tresult.Subscriptions = append(result.Subscriptions, AzureSubscriptionInfo{\n\t\t\tID: sub.ID,\n\t\t\tName: sub.Name,\n\t\t\tState: sub.State,\n\t\t\tIsDefault: sub.IsDefault,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc findAzureCLI() (string, error) {\n\tnames := []string{\"az\"}\n\tif runtime.GOOS == \"windows\" {\n\t\tnames = []string{\"az.cmd\", \"az.exe\", \"az\"}\n\t}\n\n\tfor _, name := range names {\n\t\tif p, err := exec.LookPath(name); err == nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\thome, _ := os.UserHomeDir()\n\tvar candidates []string\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcandidates = []string{\n\t\t\t\"/opt/homebrew/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"linux\":\n\t\tcandidates = []string{\n\t\t\t\"/usr/bin/az\",\n\t\t\t\"/usr/local/bin/az\",\n\t\t}\n\tcase \"windows\":\n\t\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\t\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\t\tif programFiles != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFiles, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif programFilesX86 != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(programFilesX86, \"Microsoft SDKs\", \"Azure\", \"CLI2\", \"wbin\", \"az.cmd\"))\n\t\t}\n\t\tif home != \"\" {\n\t\t\tcandidates = append(candidates, filepath.Join(home, \"AppData\", \"Local\", \"Programs\", \"Azure CLI\", \"az.cmd\"))\n\t\t}\n\t}\n\n\tfor _, p := range candidates {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif st, err := os.Stat(p); err == nil && !st.IsDir() {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\nfunc scanCloudflareCredentials() CloudflareCredentialsScan {\n\treturn CloudflareCredentialsScan{\n\t\tHasToken: os.Getenv(\"CLOUDFLARE_API_TOKEN\") != \"\",\n\t\tHasAccountID: os.Getenv(\"CLOUDFLARE_ACCOUNT_ID\") != \"\",\n\t}\n}\n\nfunc scanLLMKeys() LLMCredentialsScan {\n\treturn LLMCredentialsScan{\n\t\tOpenAI: LLMKeyStatus{HasKey: os.Getenv(\"OPENAI_API_KEY\") != \"\"},\n\t\tAnthropic: LLMKeyStatus{HasKey: os.Getenv(\"ANTHROPIC_API_KEY\") != \"\"},\n\t\tGemini: LLMKeyStatus{HasKey: os.Getenv(\"GEMINI_API_KEY\") != \"\"},\n\t}\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n\tconfigCmd.AddCommand(configScanCmd)\n\n\tconfigScanCmd.Flags().StringP(\"output\", \"o\", \"\", \"Output format (json for JSON output)\")\n}\n"}, "906ee87b55a4c67b6b1e9c975c9d0e0098fe21fb": {"t": 1785748553.3409195, "n": 5287, "h": "5980a72b660a5f01", "c": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-2.5-flash\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n gcp:\n project_id: your-gcp-project-id\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n}\n"}, "cf4301ab6ac058596143849afa18decefbdc8119": {"t": 1785748554.8070629, "n": 5237, "h": "c6e3dda6f8779c63", "c": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\t// TODO: service_keywords were removed from the default config to keep it minimal.\n\t\t// If we want keyword-based log routing, reintroduce them under `aws.service_keywords`.\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-pro\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n}\n"}, "170630e8ede5b52e5e389905e3b7d5875f2026d2": {"t": 1785748555.8768382, "n": 6295, "h": "43243447e3da206a", "c": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: openai # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-pro\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n# Service keywords for log analysis and service identification\nservice_keywords:\n api:\n - api\n - gateway\n - rest\n - graphql\n - endpoint\n - service\n web:\n - web\n - frontend\n - ui\n - webapp\n - dashboard\n auth:\n - auth\n - authentication\n - authorization\n - login\n - cognito\n - oauth\n - jwt\n - token\n database:\n - database\n - db\n - rds\n - postgres\n - mysql\n - mongodb\n - dynamodb\n - redis\n storage:\n - storage\n - s3\n - bucket\n - file\n - blob\n - object\n compute:\n - compute\n - ec2\n - lambda\n - function\n - container\n - ecs\n - fargate\n networking:\n - network\n - vpc\n - subnet\n - route\n - load-balancer\n - alb\n - nlb\n - cloudfront\n - cdn\n monitoring:\n - monitor\n - logs\n - metrics\n - cloudwatch\n - alerts\n - dashboard\n security:\n - security\n - waf\n - shield\n - firewall\n - encryption\n - kms\n - secrets\n cicd:\n - ci\n - cd\n - pipeline\n - build\n - deploy\n - github-actions\n - codebuild\n - codepipeline\n ml:\n - ml\n - ai\n - machine-learning\n - bedrock\n - sagemaker\n - model\n - inference\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\nverbose: false\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n}\n"}, "de3f784bebb00e38c5b7aeba1c9349b1fd21674b": {"t": 1785748556.9189458, "n": 6296, "h": "cd885f0b799b392b", "c": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// configCmd represents the config command\nvar configCmd = &cobra.Command{\n\tUse: \"config\",\n\tShort: \"Manage clanker configuration\",\n\tLong: `Configure clanker settings including AI provider and API keys.`,\n}\n\nvar configInitCmd = &cobra.Command{\n\tUse: \"init\",\n\tShort: \"Initialize configuration file\",\n\tLong: `Create a default configuration file in your home directory.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\t// Check if config already exists\n\t\tif _, err := os.Stat(configPath); err == nil {\n\t\t\tfmt.Printf(\"Configuration file already exists at %s\\n\", configPath)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create default config\n\t\tdefaultConfig := `# Clanker Configuration\n# Copy this to ~/.clanker.yaml and customize for your setup\n\n# AI Providers Configuration\nai:\n default_provider: bedrock # Default AI provider to use\n \n providers:\n bedrock:\n aws_profile: your-aws-profile # AWS profile for Bedrock API calls\n model: us.anthropic.claude-sonnet-4-20250514-v1:0\n region: us-west-1\n \n openai:\n model: gpt-5\n api_key_env: OPENAI_API_KEY\n \n anthropic:\n model: claude-3-sonnet-20240229\n api_key_env: ANTHROPIC_API_KEY\n \n gemini:\n project_id: your-gcp-project-id\n \n gemini-api:\n model: gemini-pro\n api_key_env: GEMINI_API_KEY\n\n# Infrastructure Providers Configuration\ninfra:\n default_environment: dev # Default environment to use\n default_provider: aws # Default infrastructure provider\n \n aws:\n environments:\n dev:\n profile: your-dev-profile\n region: us-east-1\n description: Development environment\n stage:\n profile: your-stage-profile\n region: us-east-1\n description: Staging environment\n prod:\n profile: your-prod-profile\n region: us-east-1\n description: Production environment\n\n# Service keywords for log analysis and service identification\nservice_keywords:\n api:\n - api\n - gateway\n - rest\n - graphql\n - endpoint\n - service\n web:\n - web\n - frontend\n - ui\n - webapp\n - dashboard\n auth:\n - auth\n - authentication\n - authorization\n - login\n - cognito\n - oauth\n - jwt\n - token\n database:\n - database\n - db\n - rds\n - postgres\n - mysql\n - mongodb\n - dynamodb\n - redis\n storage:\n - storage\n - s3\n - bucket\n - file\n - blob\n - object\n compute:\n - compute\n - ec2\n - lambda\n - function\n - container\n - ecs\n - fargate\n networking:\n - network\n - vpc\n - subnet\n - route\n - load-balancer\n - alb\n - nlb\n - cloudfront\n - cdn\n monitoring:\n - monitor\n - logs\n - metrics\n - cloudwatch\n - alerts\n - dashboard\n security:\n - security\n - waf\n - shield\n - firewall\n - encryption\n - kms\n - secrets\n cicd:\n - ci\n - cd\n - pipeline\n - build\n - deploy\n - github-actions\n - codebuild\n - codepipeline\n ml:\n - ml\n - ai\n - machine-learning\n - bedrock\n - sagemaker\n - model\n - inference\n\ngithub:\n token: \"\" # GitHub personal access token (optional for public repos)\n default_repo: your-repo # Default repository to use\n repos: # List of GitHub repositories\n - owner: your-username\n repo: your-infrastructure-repo\n description: Infrastructure repository\n - owner: your-username\n repo: your-services-repo\n description: Services and database schemas\n - owner: your-username\n repo: your-app-repo\n description: Application repository\n\npostgres:\n default_connection: dev # Default PostgreSQL connection\n connections: # PostgreSQL connections\n dev:\n host: localhost\n port: 5432\n database: your_dev_db\n username: postgres\n description: Development database\n stage:\n host: your-stage-db.example.com\n port: 5432\n database: your_stage_db\n username: app_user\n description: Staging database\n\nterraform:\n default_workspace: dev # Default Terraform workspace\n workspaces: # Terraform workspaces\n dev:\n path: /path/to/your/infrastructure\n description: Development infrastructure\n stage:\n path: /path/to/your/infrastructure\n description: Staging infrastructure\n\ncodebase:\n paths: # Paths to scan for code analysis\n - .\n - /path/to/your/services\n - /path/to/your/infrastructure\n exclude: # Patterns to exclude\n - node_modules\n - .git\n - vendor\n - __pycache__\n - \"*.log\"\n - \"*.tmp\"\n - \".env*\"\n max_file_size: 1048576 # Max file size to analyze (1MB)\n max_files: 100 # Max number of files to analyze per query\n\n# General settings\nverbose: false\ntimeout: 30 # Timeout for AI requests in seconds\n`\n\n\t\terr = os.WriteFile(configPath, []byte(defaultConfig), 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file created at %s\\n\", configPath)\n\t\tfmt.Println(\"Please edit the file to add your AI provider API key.\")\n\t\treturn nil\n\t},\n}\n\nvar configShowCmd = &cobra.Command{\n\tUse: \"show\",\n\tShort: \"Show current configuration\",\n\tLong: `Display the current configuration settings.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error finding home directory: %w\", err)\n\t\t}\n\n\t\tconfigPath := filepath.Join(home, \".clanker.yaml\")\n\n\t\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\t\tfmt.Println(\"No configuration file found. Run 'clanker config init' to create one.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading config file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"Configuration file: %s\\n\\n\", configPath)\n\t\tfmt.Print(string(content))\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(configInitCmd)\n\tconfigCmd.AddCommand(configShowCmd)\n}\n"}}, "cmd/ask.go": {"4d7af4d3f13f7651fb538328b6d57eacdfc4f8d7": {"t": 1785745619.356229, "n": 140760, "h": "175c0c98ed05ad51", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/oracle\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeOracle || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"oracle\":\n\t\tincludeOracle = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your cloud infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeOracle, _ := cmd.Flags().GetBool(\"oracle\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeObservability, _ := cmd.Flags().GetBool(\"observability\")\n\t\tobservabilityRequestedExplicitly := includeObservability\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif !includeObservability && shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\tincludeObservability = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tif observabilityRequestedExplicitly {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"agent-observability\",\n\t\t\t\t\t\"reason\": \"--observability requested logs, traces, metrics, alerts, errors, and warnings context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"observability\" {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"oracle\") {\n\t\t\t\tprofile := oracle.ResolveProfile()\n\t\t\t\treturn maker.ExecuteOraclePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tOracleProfile: profile,\n\t\t\t\t\tOracleCompartmentID: oracle.ResolveCompartmentID(),\n\t\t\t\t\tOracleTenancyOCID: oracle.ResolveTenancyOCID(profile),\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitOracle := cmd.Flags().Changed(\"oracle\") && includeOracle\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitOracle {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --oracle, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitOracle:\n\t\t\t\tmakerProvider = \"oracle\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Oracle {\n\t\t\t\t\tmakerProvider = \"oracle\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"oracle\":\n\t\t\t\tprompt = maker.OraclePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Oracle, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"oracle\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeOracle,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\tif includeObservability {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --oracle flag\n\t\tif includeOracle && !makerMode {\n\t\t\treturn handleOracleQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeOracle && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v, Oracle=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare, svcCtx.Oracle)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v, Oracle=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare, svcCtx.Oracle)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Oracle Cloud queries\n\t\t\tif svcCtx.Oracle {\n\t\t\t\treturn handleOracleQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\t\treturn handleObservabilityQuery(context.Background(), routingQuestion, debug, profile)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"oracle\", false, \"Include Oracle Cloud Infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().Bool(\"observability\", false, \"Route query to observability agent for logs, traces, metrics, alerts, errors, and warnings\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Oracle, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleOracleQuery delegates an Oracle Cloud Infrastructure query to the OCI CLI client.\nfunc handleOracleQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Oracle Cloud Infrastructure agent...\")\n\t}\n\n\tclient, err := oracle.NewClient(oracle.ResolveProfile(), oracle.ResolveCompartmentID(), oracle.ResolveTenancyOCID(oracle.ResolveProfile()), debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Oracle Cloud client: %w\", err)\n\t}\n\n\toracleContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Oracle Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\tprompt := fmt.Sprintf(`You are an Oracle Cloud Infrastructure expert. Answer the user's question based on the following OCI context data.\n\nOracle Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite compartment OCIDs, resource OCIDs, regions, and lifecycle states when relevant. If the data is insufficient, say which OCI CLI command or missing IAM policy would surface it.`, oracleContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestion = routeOnlyUserQuestion(question)\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tif shouldRouteToObservabilityAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-observability\", Reason: \"Observability request detected\"}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc routeOnlyUserQuestion(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\tlower := strings.ToLower(trimmed)\n\tcut := len(trimmed)\n\tfor _, marker := range []string{\n\t\t\"\\n\\ncurrent infrastructure context\",\n\t\t\"\\ncurrent infrastructure context\",\n\t\t\"\\n\\ninfra_summary\",\n\t\t\"\\ninfra_summary\",\n\t\t\"\\n\\ndatabase_estate_resources\",\n\t\t\"\\ndatabase_estate_resources\",\n\t\t\"\\n\\nrecent conversation history\",\n\t\t\"\\nrecent conversation history\",\n\t} {\n\t\tif idx := strings.Index(lower, marker); idx >= 0 && idx < cut {\n\t\t\tcut = idx\n\t\t}\n\t}\n\treturn strings.TrimSpace(trimmed[:cut])\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "1986894da2e62a39c129e5fc0d954c8666020e05": {"t": 1785748530.0005019, "n": 140760, "h": "175c0c98ed05ad51", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/oracle\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeOracle || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"oracle\":\n\t\tincludeOracle = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your cloud infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeOracle, _ := cmd.Flags().GetBool(\"oracle\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeObservability, _ := cmd.Flags().GetBool(\"observability\")\n\t\tobservabilityRequestedExplicitly := includeObservability\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif !includeObservability && shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\tincludeObservability = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tif observabilityRequestedExplicitly {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"agent-observability\",\n\t\t\t\t\t\"reason\": \"--observability requested logs, traces, metrics, alerts, errors, and warnings context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"observability\" {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"oracle\") {\n\t\t\t\tprofile := oracle.ResolveProfile()\n\t\t\t\treturn maker.ExecuteOraclePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tOracleProfile: profile,\n\t\t\t\t\tOracleCompartmentID: oracle.ResolveCompartmentID(),\n\t\t\t\t\tOracleTenancyOCID: oracle.ResolveTenancyOCID(profile),\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitOracle := cmd.Flags().Changed(\"oracle\") && includeOracle\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitOracle {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --oracle, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitOracle:\n\t\t\t\tmakerProvider = \"oracle\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Oracle {\n\t\t\t\t\tmakerProvider = \"oracle\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"oracle\":\n\t\t\t\tprompt = maker.OraclePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Oracle, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"oracle\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeOracle, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeOracle,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\tif includeObservability {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --oracle flag\n\t\tif includeOracle && !makerMode {\n\t\t\treturn handleOracleQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeOracle && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v, Oracle=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare, svcCtx.Oracle)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v, Oracle=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare, svcCtx.Oracle)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Oracle Cloud queries\n\t\t\tif svcCtx.Oracle {\n\t\t\t\treturn handleOracleQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\t\treturn handleObservabilityQuery(context.Background(), routingQuestion, debug, profile)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"oracle\", false, \"Include Oracle Cloud Infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().Bool(\"observability\", false, \"Route query to observability agent for logs, traces, metrics, alerts, errors, and warnings\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Oracle, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleOracleQuery delegates an Oracle Cloud Infrastructure query to the OCI CLI client.\nfunc handleOracleQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Oracle Cloud Infrastructure agent...\")\n\t}\n\n\tclient, err := oracle.NewClient(oracle.ResolveProfile(), oracle.ResolveCompartmentID(), oracle.ResolveTenancyOCID(oracle.ResolveProfile()), debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Oracle Cloud client: %w\", err)\n\t}\n\n\toracleContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Oracle Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\tprompt := fmt.Sprintf(`You are an Oracle Cloud Infrastructure expert. Answer the user's question based on the following OCI context data.\n\nOracle Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite compartment OCIDs, resource OCIDs, regions, and lifecycle states when relevant. If the data is insufficient, say which OCI CLI command or missing IAM policy would surface it.`, oracleContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestion = routeOnlyUserQuestion(question)\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tif shouldRouteToObservabilityAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-observability\", Reason: \"Observability request detected\"}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc routeOnlyUserQuestion(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\tlower := strings.ToLower(trimmed)\n\tcut := len(trimmed)\n\tfor _, marker := range []string{\n\t\t\"\\n\\ncurrent infrastructure context\",\n\t\t\"\\ncurrent infrastructure context\",\n\t\t\"\\n\\ninfra_summary\",\n\t\t\"\\ninfra_summary\",\n\t\t\"\\n\\ndatabase_estate_resources\",\n\t\t\"\\ndatabase_estate_resources\",\n\t\t\"\\n\\nrecent conversation history\",\n\t\t\"\\nrecent conversation history\",\n\t} {\n\t\tif idx := strings.Index(lower, marker); idx >= 0 && idx < cut {\n\t\t\tcut = idx\n\t\t}\n\t}\n\treturn strings.TrimSpace(trimmed[:cut])\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "00ec8d9d8cc72ecf8ad9f57627cb16edc012176e": {"t": 1785748531.5103607, "n": 137375, "h": "7a3c56f371fed026", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeObservability, _ := cmd.Flags().GetBool(\"observability\")\n\t\tobservabilityRequestedExplicitly := includeObservability\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif !includeObservability && shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\tincludeObservability = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tif observabilityRequestedExplicitly {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"agent-observability\",\n\t\t\t\t\t\"reason\": \"--observability requested logs, traces, metrics, alerts, errors, and warnings context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"observability\" {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\tif includeObservability {\n\t\t\treturn handleObservabilityQuery(context.Background(), question, debug, profile)\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToObservabilityAgent(routingQuestion) {\n\t\t\t\treturn handleObservabilityQuery(context.Background(), routingQuestion, debug, profile)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().Bool(\"observability\", false, \"Route query to observability agent for logs, traces, metrics, alerts, errors, and warnings\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, observability, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestion = routeOnlyUserQuestion(question)\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tif shouldRouteToObservabilityAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-observability\", Reason: \"Observability request detected\"}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc routeOnlyUserQuestion(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\tlower := strings.ToLower(trimmed)\n\tcut := len(trimmed)\n\tfor _, marker := range []string{\n\t\t\"\\n\\ncurrent infrastructure context\",\n\t\t\"\\ncurrent infrastructure context\",\n\t\t\"\\n\\ninfra_summary\",\n\t\t\"\\ninfra_summary\",\n\t\t\"\\n\\ndatabase_estate_resources\",\n\t\t\"\\ndatabase_estate_resources\",\n\t\t\"\\n\\nrecent conversation history\",\n\t\t\"\\nrecent conversation history\",\n\t} {\n\t\tif idx := strings.Index(lower, marker); idx >= 0 && idx < cut {\n\t\t\tcut = idx\n\t\t}\n\t}\n\treturn strings.TrimSpace(trimmed[:cut])\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "38ea0ea3b1abd71e666c7e9be209a0d18021cbda": {"t": 1785748533.0193722, "n": 136149, "h": "05037384841014ed", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestion = routeOnlyUserQuestion(question)\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc routeOnlyUserQuestion(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\tlower := strings.ToLower(trimmed)\n\tcut := len(trimmed)\n\tfor _, marker := range []string{\n\t\t\"\\n\\ncurrent infrastructure context\",\n\t\t\"\\ncurrent infrastructure context\",\n\t\t\"\\n\\ninfra_summary\",\n\t\t\"\\ninfra_summary\",\n\t\t\"\\n\\ndatabase_estate_resources\",\n\t\t\"\\ndatabase_estate_resources\",\n\t\t\"\\n\\nrecent conversation history\",\n\t\t\"\\nrecent conversation history\",\n\t} {\n\t\tif idx := strings.Index(lower, marker); idx >= 0 && idx < cut {\n\t\t\tcut = idx\n\t\t}\n\t}\n\treturn strings.TrimSpace(trimmed[:cut])\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "b6a445505faaf64ad7840a87b42b7ca254e42d3d": {"t": 1785748534.5224152, "n": 135490, "h": "4b84a8294e827d6c", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "2aa4d347ad28f3829407d405ddbcde53f51f2702": {"t": 1785748536.0430682, "n": 135490, "h": "222ae00f4478c59d", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/tencent\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTencent, _ := cmd.Flags().GetBool(\"tencent\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"tencent\") {\n\t\t\t\ttcCreds := tencent.ResolveCredentials()\n\t\t\t\tif tcCreds.SecretID == \"\" || tcCreds.SecretKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"tencent credentials are required for --apply (set tencent.secret_id / tencent.secret_key, TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY, or TENCENT_SECRET_ID / TENCENT_SECRET_KEY)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteTencentPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tTencentSecretID: tcCreds.SecretID,\n\t\t\t\t\tTencentSecretKey: tcCreds.SecretKey,\n\t\t\t\t\tTencentRegion: tcCreds.Region,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitTencent := cmd.Flags().Changed(\"tencent\") && includeTencent\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitTencent {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda, --tencent) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitTencent:\n\t\t\t\tmakerProvider = \"tencent\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"tencent\":\n\t\t\t\tprompt = maker.TencentPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" || providerLower == \"tencent\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --tencent flag\n\t\tif includeTencent && !makerMode {\n\t\t\treturn handleTencentQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"tencent\", false, \"Include Tencent Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleTencentQuery delegates a Tencent Cloud query to the tencent client.\n// Mirrors handleDigitalOceanQuery: gather relevant context from the SDK,\n// stuff it into the prompt, hand to the configured AI provider.\nfunc handleTencentQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Tencent Cloud agent...\")\n\t}\n\n\tcreds := tencent.ResolveCredentials()\n\tclient, err := tencent.NewClient(creds, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Tencent Cloud context: %w\", err)\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Tencent Cloud infrastructure expert. Answer the user's question using the inventory data below. Tencent service abbreviations: CVM=Cloud Virtual Machine, VPC=Virtual Private Cloud, SG=Security Group, COS=Cloud Object Storage, CLB=Cloud Load Balancer, TKE=Tencent Kubernetes Engine, CDB=TencentDB for MySQL.\n\nTencent Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. Cite specific resource IDs (ins-*, vpc-*, sg-*) when relevant. If the data is insufficient, say what is missing and suggest the specific clanker subcommand that would surface it (e.g. clanker tencent list cvm --all-regions, clanker tencent sg-rules ).`, tcContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "4b96c1cbb212fb8b5a547529b0bfbd53e078c405": {"t": 1785748537.5724437, "n": 132033, "h": "5c4685b4c8782885", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\t// NOTE: bare \"schema\"/\"schemas\", \"column\"/\"columns\", \"table\" (singular),\n\t// \"index\"/\"indexes\" intentionally dropped from these lists. They produced\n\t// false-positive DB-context inclusion for GraphQL/JSON/zod schemas, search\n\t// indexes, doc reindexing, and ambiguous table mentions. The remaining\n\t// matchers cover the real cases:\n\t// • Engine names (\"supabase\", \"neon\", \"sqlite\", \"sqlite3\") + the\n\t// postgres/mysql combination check below.\n\t// • Multi-word phrases that are unambiguously DB-related\n\t// (\"sql query\", \"sql schema\", \"foreign key\", \"primary key\",\n\t// \"database connection\", \"db connection\", \"migration\").\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific).\n\t// NOTE: dropped \"postgres\", \"mysql\", \"redis\", \"mongodb\" from this list.\n\t// They are database engines that happen to be deployable on K8s, but\n\t// also exist as managed services (AWS RDS / ElastiCache, GCP Cloud SQL,\n\t// Supabase, etc.) and as in-process libraries. Including them here was\n\t// causing \"create a postgres table\" / \"spin up a redis cluster\" to\n\t// route to `k8s-maker` ahead of `maker` or `agent-database` — wrong in\n\t// the common case. With these removed, those queries fall through to\n\t// the AWS-resources check (maker) or the database-routing check below.\n\t// \"nginx\" stays — it's overwhelmingly used as a K8s workload today;\n\t// people running nginx on a VM tend to say \"an nginx server\" or\n\t// \"configure nginx\" which still works via the cli fallback.\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// CICD intent (github actions / workflow / pipeline / runner / cloud\n\t// build / etc.) takes precedence over the hasAction+resource check\n\t// below, even when \"deployment\" is in the question. Otherwise queries\n\t// like \"setup github actions for deployment\" route to k8s-maker\n\t// (because \"deployment\" overlaps with the K8s resources list and\n\t// \"setup\" is an action verb) — wrong, because \"github actions\"\n\t// unambiguously signals CI/CD. shouldRouteToCICDAgent already guards\n\t// against database-context overlap (sql/schema/migration), so this\n\t// early-out is safe.\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "31274b5e095383becc0a5b2ef9dc95de82e1bc8d": {"t": 1785748539.1009054, "n": 130012, "h": "0eff63bb9a457289", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tsreMode, _ := cmd.Flags().GetBool(\"sre\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif sreMode {\n\t\t\tdiscovery = true\n\t\t}\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tif sreMode {\n\t\t\t\treturn json.NewEncoder(os.Stdout).Encode(map[string]string{\n\t\t\t\t\t\"agent\": \"sre\",\n\t\t\t\t\t\"reason\": \"--sre requested adaptive SRE discovery/runtime context\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"sre\", false, \"Use adaptive Clanker SRE discovery context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "ad31d1577e654b305eef9d0228695fba43568eec": {"t": 1785748541.0952914, "n": 129662, "h": "82ea658b37dcb598", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/flyio\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeFlyio, _ := cmd.Flags().GetBool(\"flyio\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"flyio\") {\n\t\t\t\tflyToken, flyOrg, flyErr := resolveFlyioToken(ctx, debug)\n\t\t\t\tif flyErr != nil {\n\t\t\t\t\treturn flyErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteFlyioPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tFlyioAPIToken: flyToken,\n\t\t\t\t\tFlyioOrgSlug: flyOrg,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Flyio {\n\t\t\t\t\tmakerProvider = \"flyio\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --flyio flag\n\t\tif includeFlyio && !makerMode {\n\t\t\treturn handleFlyioQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeFlyio && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Fly.io queries\n\t\t\tif svcCtx.Flyio {\n\t\t\t\treturn handleFlyioQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"flyio\", false, \"Include Fly.io context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveFlyioToken resolves the Fly.io API token and optional org slug from\n// config or environment, falling back to the backend credential store when\n// configured. Resolution chain:\n// - flyio.api_token (viper) → FLY_API_TOKEN → FLY_ACCESS_TOKEN\n// - flyio.org_slug (viper) → FLY_ORG → FLY_ORG_SLUG\n//\n// Empty org slug is valid — Fly tokens see resources across every org they\n// can access; the slug is a filter, not a scope.\nfunc resolveFlyioToken(ctx context.Context, debug bool) (apiToken string, orgSlug string, err error) {\n\tapiToken = flyio.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, flyio.ResolveOrgSlug(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetFlyioCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Fly.io credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.OrgSlug), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Fly.io credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Fly.io token not configured. Set flyio.api_token in ~/.clanker.yaml or export FLY_API_TOKEN\")\n}\n\n// handleFlyioQuery delegates a Fly.io query to the Fly.io agent with per-org\n// conversation history for multi-turn context.\nfunc handleFlyioQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Fly.io agent...\")\n\t}\n\n\tapiToken, orgSlug, err := resolveFlyioToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := flyio.NewClient(apiToken, orgSlug, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Fly.io client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by org slug (or \"personal\" when unscoped).\n\tconversationID := orgSlug\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := flyio.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tflyioContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[flyio] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(flyioContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Fly.io context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildFlyioPrompt(question, flyioContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Fly.io AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildFlyioPrompt assembles the system prompt for a Fly.io ask query,\n// injecting infrastructure context and recent conversation history.\nfunc buildFlyioPrompt(question, flyioContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Fly.io infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Fly.io apps, machines, volumes, and addons based on the provided context. \")\n\tsb.WriteString(\"Fly.io organizes resources as: organizations contain apps, apps contain machines (VMs) and volumes, machines run in specific regions, and addons (Postgres, Redis, Tigris) attach to apps.\\n\\n\")\n\tif flyioContext != \"\" {\n\t\tsb.WriteString(\"Fly.io Context:\\n\")\n\t\tsb.WriteString(flyioContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "407a1a053684034ee843b026613c55628a7a7c88": {"t": 1785748542.6360304, "n": 124082, "h": "b2021629790e5113", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"software-blocks\" {\n\t\t\treturn handleSoftwareBlocksQuery(context.Background(), question, debug)\n\t\t} else if agentName == \"data_flow\" {\n\t\t\treturn handleDataFlowQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, software-blocks, data_flow, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "83beab9e29c9c0259f64ccde2cd0d7a2387056c0": {"t": 1785748545.607251, "n": 123798, "h": "cba607e459f9cace", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/railway\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda || includeRailway {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tcase \"railway\":\n\t\tincludeRailway = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeRailway, _ := cmd.Flags().GetBool(\"railway\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"railway\") {\n\t\t\t\trwToken, rwWorkspaceID, rwErr := resolveRailwayToken(ctx, debug)\n\t\t\t\tif rwErr != nil {\n\t\t\t\t\treturn rwErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteRailwayPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tRailwayAPIToken: rwToken,\n\t\t\t\t\tRailwayWorkspaceID: rwWorkspaceID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitRailway := cmd.Flags().Changed(\"railway\") && includeRailway\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitRailway {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --railway, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitRailway:\n\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Railway {\n\t\t\t\t\tmakerProvider = \"railway\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"railway\":\n\t\t\t\tprompt = maker.RailwayPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Verda, and Railway plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" || providerLower == \"railway\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda, includeRailway = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t\tincludeRailway,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --railway flag\n\t\tif includeRailway && !makerMode {\n\t\t\treturn handleRailwayQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeRailway && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Railway queries\n\t\t\tif svcCtx.Railway {\n\t\t\t\treturn handleRailwayQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"railway\", false, \"Include Railway context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, Railway, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveRailwayToken resolves the Railway account token and optional workspace\n// ID from config or environment. Workspace ID may be empty for single-workspace\n// accounts; the GraphQL API infers scope from the token in that case.\nfunc resolveRailwayToken(ctx context.Context, debug bool) (apiToken string, workspaceID string, err error) {\n\tapiToken = railway.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, railway.ResolveWorkspaceID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetRailwayCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Railway credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.WorkspaceID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Railway credentials available (%v), falling back to local\\n\", bErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Railway token not configured. Set railway.api_token in ~/.clanker.yaml or export RAILWAY_API_TOKEN\")\n}\n\n// handleRailwayQuery delegates a Railway query to the Railway agent with\n// per-workspace conversation history for multi-turn context.\nfunc handleRailwayQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Railway agent...\")\n\t}\n\n\tapiToken, workspaceID, err := resolveRailwayToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := railway.NewClient(apiToken, workspaceID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Railway client: %w\", err)\n\t}\n\n\tconversationID := workspaceID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := railway.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\trailwayContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[railway] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(railwayContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Railway context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildRailwayPrompt(question, railwayContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Railway AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildRailwayPrompt assembles the system prompt for a Railway ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildRailwayPrompt(question, railwayContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Railway infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Railway workspace (projects, services, environments, deployments, domains, variables, volumes) based on the provided context.\\n\\n\")\n\tif railwayContext != \"\" {\n\t\tsb.WriteString(\"Railway Context:\\n\")\n\t\tsb.WriteString(railwayContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "ee0e660e00f74cc580dafccb71ce8e88e0117951": {"t": 1785748547.1890686, "n": 118053, "h": "37428c816540d8e6", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/bgdnvk/clanker/internal/verda\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda bool) (bool, bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel || includeVerda {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tcase \"verda\":\n\t\tincludeVerda = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeVerda, _ := cmd.Flags().GetBool(\"verda\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"verda\") {\n\t\t\t\tverdaClientID, verdaClientSecret, verdaProjectID, vErr := resolveVerdaCredentialsWithContext(ctx, debug)\n\t\t\t\tif vErr != nil {\n\t\t\t\t\treturn vErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVerdaPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVerdaClientID: verdaClientID,\n\t\t\t\t\tVerdaClientSecret: verdaClientSecret,\n\t\t\t\t\tVerdaProjectID: verdaProjectID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitVerda := cmd.Flags().Changed(\"verda\") && includeVerda\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVerda {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel, --verda) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVerda:\n\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Verda {\n\t\t\t\t\tmakerProvider = \"verda\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"verda\":\n\t\t\t\tprompt = maker.VerdaPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, and Verda plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" || providerLower == \"verda\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel, includeVerda = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t\tincludeVerda,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --verda flag\n\t\tif includeVerda && !makerMode {\n\t\t\treturn handleVerdaQuery(cmd.Context(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeVerda && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Verda queries\n\t\t\tif svcCtx.Verda {\n\t\t\t\treturn handleVerdaQuery(cmd.Context(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"verda\", false, \"Include Verda Cloud (GPU/AI) infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, Hetzner, Vercel, or Verda plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// resolveVerdaCredentials returns the Verda client ID / client secret / project ID\n// resolving in this order: ~/.clanker.yaml (verda.* keys) → VERDA_* env vars →\n// ~/.verda/credentials (written by `verda auth login`).\nfunc resolveVerdaCredentials() (clientID, clientSecret, projectID string, err error) {\n\treturn resolveVerdaCredentialsWithContext(context.Background(), false)\n}\n\n// resolveVerdaCredentialsWithContext mirrors resolveVercelToken's fallback\n// chain: local config / env / ~/.verda/credentials first, then the clanker\n// backend credential store if the local path has nothing. `debug` controls\n// whether the backend fallback logs its decision.\nfunc resolveVerdaCredentialsWithContext(ctx context.Context, debug bool) (clientID, clientSecret, projectID string, err error) {\n\tclientID = verda.ResolveClientID()\n\tclientSecret = verda.ResolveClientSecret()\n\tprojectID = verda.ResolveProjectID()\n\tif clientID != \"\" && clientSecret != \"\" {\n\t\treturn clientID, clientSecret, projectID, nil\n\t}\n\n\t// Local resolution failed — try the clanker backend credential store if\n\t// the user has an API key configured. If the backend route isn't\n\t// provisioned server-side yet we get a 404 and fall through to the\n\t// human-readable error below.\n\tif backendAPIKey := backend.ResolveAPIKey(\"\"); backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, bErr := backendClient.GetVerdaCredentials(ctx)\n\t\tif bErr == nil && strings.TrimSpace(creds.ClientID) != \"\" && strings.TrimSpace(creds.ClientSecret) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Verda credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.ClientID), strings.TrimSpace(creds.ClientSecret), strings.TrimSpace(creds.ProjectID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Verda credentials available (%v), falling back to local error\\n\", bErr)\n\t\t}\n\t}\n\n\t// Pick the most-likely-useful suggestion based on whether the verda\n\t// CLI is installed. Users without the CLI should never be told to run\n\t// `verda auth login` — they'll get a confusing \"command not found\".\n\tsuggestion := \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml or export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET.\"\n\tif verda.CLIInstalled() {\n\t\tsuggestion = \"Set verda.client_id / verda.client_secret in ~/.clanker.yaml, export VERDA_CLIENT_ID / VERDA_CLIENT_SECRET, or run `verda auth login` (the CLI writes ~/.verda/credentials which clanker reads automatically).\"\n\t}\n\treturn \"\", \"\", \"\", fmt.Errorf(\"Verda credentials not configured. %s\", suggestion)\n}\n\n// handleVerdaQuery delegates a Verda Cloud query to the Verda agent with\n// per-project conversation history for multi-turn context.\nfunc handleVerdaQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Verda agent...\")\n\t}\n\n\tclientID, clientSecret, projectID, err := resolveVerdaCredentialsWithContext(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := verda.NewClient(clientID, clientSecret, projectID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Verda client: %w\", err)\n\t}\n\n\tscope := projectID\n\tif scope == \"\" {\n\t\tscope = \"personal\"\n\t}\n\thistory := verda.NewConversationHistory(scope)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] verda conversation history: %v\\n\", err)\n\t}\n\n\tverdaContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[verda] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(verdaContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Verda context: %w\", err)\n\t\t}\n\t}\n\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVerdaPrompt(question, verdaContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Verda AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save verda history: %v\\n\", err)\n\t}\n\treturn nil\n}\n\n// buildVerdaPrompt assembles the system prompt for a Verda ask query, injecting\n// resource context and recent conversation history when available.\nfunc buildVerdaPrompt(question, verdaContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Verda Cloud infrastructure assistant. \")\n\tsb.WriteString(\"Verda (ex-DataCrunch) is a European GPU/AI cloud that provides GPU instances (H100, A100, H200, B200, L40S, V100, A6000), Instant Clusters (with Slurm or Kubernetes orchestrator), volumes (including shared filesystems), serverless containers and jobs. Answer questions about the user's Verda account based on the provided context.\\n\\n\")\n\tif verdaContext != \"\" {\n\t\tsb.WriteString(\"Verda Context:\\n\")\n\t\tsb.WriteString(verdaContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format. When discussing pricing, remember Verda hourly prices can be converted to monthly using *730.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "34bc2e47c9a9f66b7ea84d8ac51c4395e1ebb62c": {"t": 1785748548.931206, "n": 110610, "h": "70a203bd955e8448", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel bool) (bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent with\n// per-team conversation history for multi-turn context.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\t// Load conversation history keyed by team (or \"personal\" for non-team accounts).\n\tconversationID := teamID\n\tif conversationID == \"\" {\n\t\tconversationID = \"personal\"\n\t}\n\thistory := vercel.NewConversationHistory(conversationID)\n\tif err := history.Load(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] conversation history: %v\\n\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\thistoryContext := history.GetRecentContext(5)\n\tprompt := buildVercelPrompt(question, vercelContext, historyContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\n\t// Persist the exchange so subsequent invocations see the conversation.\n\thistory.AddEntry(question, response)\n\tif err := history.Save(); err != nil && debug {\n\t\tfmt.Fprintf(os.Stderr, \"[debug] save history: %v\\n\", err)\n\t}\n\n\treturn nil\n}\n\n// buildVercelPrompt assembles the system prompt for a Vercel ask query,\n// injecting infrastructure context and recent conversation history when\n// available.\nfunc buildVercelPrompt(question, vercelContext, historyContext string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"You are a Vercel infrastructure assistant. \")\n\tsb.WriteString(\"Answer questions about the user's Vercel account based on the provided context.\\n\\n\")\n\tif vercelContext != \"\" {\n\t\tsb.WriteString(\"Vercel Context:\\n\")\n\t\tsb.WriteString(vercelContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tif historyContext != \"\" {\n\t\tsb.WriteString(\"Recent Conversation:\\n\")\n\t\tsb.WriteString(historyContext)\n\t\tsb.WriteString(\"\\n\\n\")\n\t}\n\tsb.WriteString(\"User Question: \")\n\tsb.WriteString(question)\n\tsb.WriteString(\"\\n\\nProvide a helpful, concise response in markdown format.\")\n\treturn sb.String()\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "0a08404f05431cb6f527a74e363d86f8469305dd": {"t": 1785748550.3766577, "n": 109356, "h": "6852b97211d1decb", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel bool) (bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent.\n// One-shot Q&A (no conversation history) — full history lands in phase 4.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t\tif strings.TrimSpace(vercelContext) == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to fetch Vercel context: %w\", err)\n\t\t}\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Vercel infrastructure assistant. Answer questions about the user's Vercel account based on the provided context.\n\nVercel Context:\n%s\n\nUser Question: %s\n\nProvide a helpful, concise response in markdown format.`, vercelContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "ba878d6245232aa105156650bdb04bf1889bbe73": {"t": 1785748551.848163, "n": 109242, "h": "6c82964ae70a36f0", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel bool) (bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken, vcTeamID, vcErr := resolveVercelToken(ctx, debug)\n\t\t\t\tif vcErr != nil {\n\t\t\t\t\treturn vcErr\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Provider-specific Q&A paths.\n\t\t// NOTE: makerMode returns above, so these only fire for\n\t\t// plain -- queries (not --maker --).\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare && !makerMode {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean && !makerMode {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner && !makerMode {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel && !makerMode {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// resolveVercelToken resolves the Vercel API token from config, env, or the\n// backend credential store (same fallback chain as resolveHetznerToken).\n// The returned teamID may be empty for personal accounts.\nfunc resolveVercelToken(ctx context.Context, debug bool) (apiToken string, teamID string, err error) {\n\tapiToken = vercel.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, vercel.ResolveTeamID(), nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tcreds, backendErr := backendClient.GetVercelCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(creds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Vercel credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(creds.APIToken), strings.TrimSpace(creds.TeamID), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Vercel credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent.\n// One-shot Q&A (no conversation history) — full history lands in phase 4.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken, teamID, err := resolveVercelToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"[vercel] warning: failed to fetch context: %v\\n\", err)\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Vercel infrastructure assistant. Answer questions about the user's Vercel account based on the provided context.\n\nVercel Context:\n%s\n\nUser Question: %s\n\nProvide a helpful, concise response in markdown format.`, vercelContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "57b1c7f1f43000abfad99a05c8e38159dde51104": {"t": 1785748553.4095376, "n": 108151, "h": "8e556493f6cf6350", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel bool) (bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"vercel\") {\n\t\t\t\tvcToken := vercel.ResolveAPIToken()\n\t\t\t\tif vcToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"vercel api_token is required (set vercel.api_token, VERCEL_TOKEN, or --api-token)\")\n\t\t\t\t}\n\t\t\t\tvcTeamID := vercel.ResolveTeamID()\n\t\t\t\treturn maker.ExecuteVercelPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tVercelAPIToken: vcToken,\n\t\t\t\t\tVercelTeamID: vcTeamID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\tprompt = maker.VercelPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" || providerLower == \"vercel\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent.\n// One-shot Q&A (no conversation history) — full history lands in phase 4.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken := vercel.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n\t}\n\tteamID := vercel.ResolveTeamID()\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil && debug {\n\t\tfmt.Printf(\"[debug] failed to get Vercel context: %v\\n\", err)\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Vercel infrastructure assistant. Answer questions about the user's Vercel account based on the provided context.\n\nVercel Context:\n%s\n\nUser Question: %s\n\nProvide a helpful, concise response in markdown format.`, vercelContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "2d70a85af81d2542788c9966a4768c00fc69db7e": {"t": 1785748554.8697584, "n": 107617, "h": "0128ba6af7ae87d7", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/bgdnvk/clanker/internal/vercel\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel bool) (bool, bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner || includeVercel {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tcase \"vercel\":\n\t\tincludeVercel = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeVercel, _ := cmd.Flags().GetBool(\"vercel\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitVercel := cmd.Flags().Changed(\"vercel\") && includeVercel\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitVercel {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner, --vercel) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitVercel:\n\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Vercel {\n\t\t\t\t\tmakerProvider = \"vercel\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"vercel\":\n\t\t\t\treturn fmt.Errorf(\"maker mode not yet supported for Vercel (planned for phase 2)\")\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform, includeVercel = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t\tincludeVercel,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --vercel flag\n\t\tif includeVercel {\n\t\t\treturn handleVercelQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeVercel && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Vercel queries\n\t\t\tif svcCtx.Vercel {\n\t\t\t\treturn handleVercelQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"vercel\", false, \"Include Vercel context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleVercelQuery delegates a Vercel query to the Vercel agent.\n// One-shot Q&A (no conversation history) — full history lands in phase 4.\nfunc handleVercelQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Vercel agent...\")\n\t}\n\n\tapiToken := vercel.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"Vercel token not configured. Set vercel.api_token in ~/.clanker.yaml or export VERCEL_TOKEN\")\n\t}\n\tteamID := vercel.ResolveTeamID()\n\n\tclient, err := vercel.NewClient(apiToken, teamID, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Vercel client: %w\", err)\n\t}\n\n\tvercelContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil && debug {\n\t\tfmt.Printf(\"[debug] failed to get Vercel context: %v\\n\", err)\n\t}\n\n\t// Resolve AI provider + key using the same pattern as other handlers.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\tapiKey = \"\"\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(fmt.Sprintf(\"ai.providers.%s.api_key\", provider))\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Vercel infrastructure assistant. Answer questions about the user's Vercel account based on the provided context.\n\nVercel Context:\n%s\n\nUser Question: %s\n\nProvide a helpful, concise response in markdown format.`, vercelContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Vercel AI query failed: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "cf4c8bef8b043dcb7cf98d4fa4968fc98679e536": {"t": 1785748556.3454, "n": 104681, "h": "3be7dcecff8a4824", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/dbcontext\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform bool) (bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeCICD, _ := cmd.Flags().GetBool(\"cicd\")\n\t\tincludeDB, _ := cmd.Flags().GetBool(\"db\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tdbConnection, _ := cmd.Flags().GetString(\"db-connection\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\t\tif includeCICD {\n\t\t\tincludeGitHub = true\n\t\t}\n\t\tif strings.TrimSpace(dbConnection) != \"\" {\n\t\t\tincludeDB = true\n\t\t}\n\t\tif !includeDB && shouldIncludeDatabaseContextWithContext(routingQuestion, dbConnection) {\n\t\t\tincludeDB = true\n\t\t}\n\t\tdbRequestedExplicitly := cmd.Flags().Changed(\"db\") || cmd.Flags().Changed(\"db-connection\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": decision.Agent,\n\t\t\t\t\"reason\": decision.Reason,\n\t\t\t}\n\t\t\tif decision.DatabaseMode != \"\" {\n\t\t\t\tresult[\"databaseMode\"] = decision.DatabaseMode\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"database\" {\n\t\t\treturn handleDatabaseQuery(context.Background(), question, debug, dbConnection)\n\t\t} else if agentName == \"cicd\" {\n\t\t\treturn handleCICDQuery(context.Background(), question, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, database, cicd, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner && !includeDB {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\tif shouldRouteToDatabaseAgentWithContext(routingQuestion, dbConnection) {\n\t\t\t\treturn handleDatabaseQuery(context.Background(), routingQuestion, debug, dbConnection)\n\t\t\t}\n\n\t\t\tif shouldRouteToCICDAgent(routingQuestion) {\n\t\t\t\treturn handleCICDQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\t\tvar dbContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeDB {\n\t\t\tvar err error\n\t\t\tdbContext, err = dbcontext.BuildRelevantContext(ctx, routingQuestion, dbConnection)\n\t\t\tif err != nil {\n\t\t\t\tif dbRequestedExplicitly {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get database context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get database context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tdbContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\t\tif strings.TrimSpace(dbContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Database Context:\\n\" + dbContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"cicd\", false, \"Include CI/CD context (currently GitHub Actions)\")\n\taskCmd.Flags().Bool(\"db\", false, \"Include configured database context\")\n\taskCmd.Flags().String(\"db-connection\", \"\", \"Database connection name to inspect when using --db\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, database, cicd, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\nfunc shouldIncludeDatabaseContext(question string) bool {\n\treturn shouldIncludeDatabaseContextWithContext(question, \"\")\n}\n\nfunc shouldIncludeDatabaseContextWithContext(question string, dbConnection string) bool {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false\n\t}\n\tif shouldRouteToDatabaseAgentWithContext(q, dbConnection) {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"supabase\", \"neon\", \"sqlite\", \"sqlite3\", \"database connection\", \"db connection\", \"schema\", \"schemas\", \"column\", \"columns\", \"migration\", \"migrations\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"table\", \"tables\", \"sql query\", \"sql schema\", \"foreign key\", \"primary key\", \"index\", \"indexes\") {\n\t\treturn true\n\t}\n\tif containsAnyPhrase(q, \"postgres\", \"postgresql\", \"mysql\") && containsAnyPhrase(q, \"table\", \"tables\", \"schema\", \"schemas\", \"column\", \"columns\", \"sql\", \"query\", \"connect\", \"connection\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc containsAnyPhrase(input string, phrases ...string) bool {\n\tfor _, phrase := range phrases {\n\t\tif strings.Contains(input, phrase) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, \"\")\n\treturn decision.Agent, decision.Reason\n}\n\nfunc determineRoutingDecisionWithContext(question string, dbConnection string) (agent string, reason string) {\n\tdecision := determineRoutingDecisionDetailsWithContext(question, dbConnection)\n\treturn decision.Agent, decision.Reason\n}\n\ntype routingDecisionDetails struct {\n\tAgent string\n\tReason string\n\tDatabaseMode string\n}\n\nfunc determineDatabaseRouteMode(questionLower string) string {\n\tif isDatabaseInfrastructureInventoryQuestion(questionLower) {\n\t\treturn \"inventory\"\n\t}\n\treturn \"query\"\n}\n\nfunc determineRoutingDecisionDetailsWithContext(question string, dbConnection string) routingDecisionDetails {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"clanker-cloud\", Reason: \"Explicit Clanker Cloud app request detected\"}\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"hermes\", Reason: \"Hermes agent explicitly requested\"}\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"iam\", Reason: \"IAM query or security analysis request\"}\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"terraform\", Reason: \"Terraform query or analysis request\"}\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn routingDecisionDetails{Agent: \"diagram\", Reason: \"Diagram or visualization request detected\"}\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn routingDecisionDetails{Agent: \"k8s-maker\", Reason: \"K8s infrastructure provisioning or modification request\"}\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn routingDecisionDetails{Agent: \"maker\", Reason: \"AWS infrastructure provisioning or modification request\"}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldRouteToDatabaseAgentWithContext(questionLower, dbConnection) {\n\t\treturn routingDecisionDetails{Agent: \"agent-database\", Reason: \"Database agent request detected\", DatabaseMode: determineDatabaseRouteMode(questionLower)}\n\t}\n\n\tif shouldRouteToCICDAgent(questionLower) {\n\t\treturn routingDecisionDetails{Agent: \"agent-cicd\", Reason: \"CI/CD agent request detected\"}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn routingDecisionDetails{Agent: \"k8s\", Reason: \"K8s query or analysis request\"}\n\t}\n\n\t// Default to CLI for general queries\n\treturn routingDecisionDetails{Agent: \"cli\", Reason: \"General infrastructure query or analysis\"}\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "e41a40bad5024b1f410caeee5ca0b749a5ad3d86": {"t": 1785748557.8337002, "n": 100068, "h": "66503126af5427ee", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform bool) (bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tlocalModelInferenceURL, _ := cmd.Flags().GetString(\"local-model-inference-url\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tif strings.TrimSpace(localModelInferenceURL) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.local_model_inference_url\", strings.TrimSpace(localModelInferenceURL))\n\t\t}\n\n\t\troutingQuestion := questionForRouting(question)\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, routingQuestion, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, routingQuestion)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"local-model-inference-url\", \"\", \"Local model inference URL for OpenAI-compatible servers (for example http://127.0.0.1:8080/v1)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "312bb27fdebcd0a3948612728d2004f15378c0f4": {"t": 1785748559.2790406, "n": 99574, "h": "e68d9e981ee18254", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform bool) (bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"bedrock\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "e48a8a0d0c826faefcba133795a4a21cf8dde094": {"t": 1785748560.9106774, "n": 99696, "h": "e691a931b26f5861", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nfunc applyDiscoveryContextDefaults(includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform bool) (bool, bool, bool, bool, bool, bool, bool) {\n\tincludeTerraform = true\n\tif includeAWS || includeGCP || includeAzure || includeCloudflare || includeDigitalOcean || includeHetzner {\n\t\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n\t}\n\n\tswitch routing.DefaultInfraProvider() {\n\tcase \"gcp\":\n\t\tincludeGCP = true\n\tcase \"azure\":\n\t\tincludeAzure = true\n\tcase \"cloudflare\":\n\t\tincludeCloudflare = true\n\tcase \"digitalocean\":\n\t\tincludeDigitalOcean = true\n\tcase \"hetzner\":\n\t\tincludeHetzner = true\n\tdefault:\n\t\tincludeAWS = true\n\t}\n\n\treturn includeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform\n}\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\tapplyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken, err := resolveHetznerToken(ctx, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := routing.DefaultInfraProvider()\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS, includeGCP, includeAzure, includeCloudflare, includeDigitalOcean, includeHetzner, includeTerraform = applyDiscoveryContextDefaults(\n\t\t\t\tincludeAWS,\n\t\t\t\tincludeGCP,\n\t\t\t\tincludeAzure,\n\t\t\t\tincludeCloudflare,\n\t\t\t\tincludeDigitalOcean,\n\t\t\t\tincludeHetzner,\n\t\t\t\tincludeTerraform,\n\t\t\t)\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: Terraform context activated alongside the selected infrastructure provider(s)\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc applyCommandAIOverrides(aiProfile, openaiKey, anthropicKey, geminiKey, deepseekKey, cohereKey, minimaxKey, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tprovider := strings.TrimSpace(aiProfile)\n\tif provider != \"\" {\n\t\tviper.Set(\"ai.default_provider\", provider)\n\t} else {\n\t\tprovider = strings.TrimSpace(viper.GetString(\"ai.default_provider\"))\n\t\tif provider == \"\" {\n\t\t\tprovider = \"openai\"\n\t\t\tviper.Set(\"ai.default_provider\", provider)\n\t\t}\n\t}\n\n\tif strings.TrimSpace(openaiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.openai.api_key\", strings.TrimSpace(openaiKey))\n\t}\n\tif strings.TrimSpace(anthropicKey) != \"\" {\n\t\tviper.Set(\"ai.providers.anthropic.api_key\", strings.TrimSpace(anthropicKey))\n\t}\n\tif strings.TrimSpace(geminiKey) != \"\" {\n\t\tviper.Set(\"ai.providers.gemini-api.api_key\", strings.TrimSpace(geminiKey))\n\t}\n\tif strings.TrimSpace(deepseekKey) != \"\" {\n\t\tviper.Set(\"ai.providers.deepseek.api_key\", strings.TrimSpace(deepseekKey))\n\t}\n\tif strings.TrimSpace(cohereKey) != \"\" {\n\t\tviper.Set(\"ai.providers.cohere.api_key\", strings.TrimSpace(cohereKey))\n\t}\n\tif strings.TrimSpace(minimaxKey) != \"\" {\n\t\tviper.Set(\"ai.providers.minimax.api_key\", strings.TrimSpace(minimaxKey))\n\t}\n\n\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\nfunc resolveHetznerToken(ctx context.Context, debug bool) (string, error) {\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken != \"\" {\n\t\treturn apiToken, nil\n\t}\n\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetHetznerCredentials(ctx)\n\t\tif backendErr == nil && strings.TrimSpace(backendCreds.APIToken) != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Hetzner credentials from backend\")\n\t\t\t}\n\t\t\treturn strings.TrimSpace(backendCreds.APIToken), nil\n\t\t}\n\t\tif debug {\n\t\t\tfmt.Printf(\"[backend] No Hetzner credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken, err := resolveHetznerToken(ctx, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, provider)\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "30a78462c64ac561478ff94d93133d51c6fcef52": {"t": 1785748562.326359, "n": 96223, "h": "930e6b9d22c9f38e", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "a8984f444ff8a8fcb7385ced9e1620363db268c0": {"t": 1785748563.7744153, "n": 96173, "h": "f3fc6589fc25ed28", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/claudecode\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName == \"claude-code\" {\n\t\t\treturn handleClaudeCodeQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, claude-code, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, claude-code, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleClaudeCodeQuery delegates a question to the locally installed Claude Code CLI.\n// When an AWS profile is available, it gathers infrastructure context first.\nfunc handleClaudeCodeQuery(ctx context.Context, question string, profile string, debug bool) error {\n\tversion, err := claudecode.CheckAvailable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif debug {\n\t\tfmt.Fprintf(os.Stderr, \"[claude-code] version: %s\\n\", version)\n\t}\n\n\trunner := claudecode.NewRunner(debug)\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[claude-code] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tevents, err := runner.Ask(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"claude-code agent error: %w\", err)\n\t}\n\n\thadDelta := false\n\tfor event := range events {\n\t\tswitch {\n\t\tcase event.Error != nil:\n\t\t\treturn fmt.Errorf(\"claude-code agent error: %w\", event.Error)\n\t\tcase event.Text != \"\":\n\t\t\tfmt.Print(event.Text)\n\t\t\thadDelta = true\n\t\tcase event.ToolCall != nil:\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[tool: %s]\\n\", event.ToolCall.Name)\n\t\t\t}\n\t\tcase event.Thought != \"\":\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[thinking: %s]\\n\", event.Thought)\n\t\t\t}\n\t\tcase event.Final != nil:\n\t\t\tif !hadDelta && event.Final.Text != \"\" {\n\t\t\t\tfmt.Print(event.Final.Text)\n\t\t\t}\n\t\t\tif debug {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n[duration: %dms, cost: $%.4f]\\n\", event.Final.DurationMS, event.Final.CostUSD)\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "ac9124c17b8fb54f306106427b2ad6f55c267714": {"t": 1785748565.2050219, "n": 93501, "h": "d11e47a957b53179", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\tif isClankerCloudQuestion(questionLower) {\n\t\treturn \"clanker-cloud\", \"Explicit Clanker Cloud app request detected\"\n\t}\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\nfunc isClankerCloudQuestion(questionLower string) bool {\n\tif strings.Contains(questionLower, \"clanker cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"clanker-cloud mcp\") ||\n\t\tstrings.Contains(questionLower, \"mcp server\") && strings.Contains(questionLower, \"clanker cloud\") {\n\t\treturn true\n\t}\n\n\texplicitSignals := []string{\n\t\t\"clanker cloud\",\n\t\t\"clanker-cloud\",\n\t\t\"clanker cloud app\",\n\t\t\"clanker cloud settings\",\n\t\t\"clanker cloud backend\",\n\t\t\"clanker cloud server\",\n\t\t\"clanker cloud mcp\",\n\t\t\"clanker-cloud mcp\",\n\t\t\"desktop app\",\n\t\t\"tauri app\",\n\t\t\"saved settings\",\n\t\t\"app settings\",\n\t\t\"running app\",\n\t\t\"local app\",\n\t}\n\tfor _, signal := range explicitSignals {\n\t\tif strings.Contains(questionLower, signal) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif strings.Contains(questionLower, \"profile\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"app\") {\n\t\treturn true\n\t}\n\tif strings.Contains(questionLower, \"settings\") && strings.Contains(questionLower, \"clanker\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "d3c3196a5f941b2a7aa816dabcbd6b0d3ab1df78": {"t": 1785748566.650512, "n": 92304, "h": "754557d035f3ea58", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.3-codex\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "b033083f56b59465b0c5711de178f071c527e100": {"t": 1785748568.0810735, "n": 92298, "h": "a44ffb73c038b167", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tselectedGitHubCodingAgent := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgithubModel, _ := cmd.Flags().GetString(\"github-model\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tgithubCodingAgentModel, _ := cmd.Flags().GetString(\"github-coding-agent-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if isGitHubCodingAgent(agentName) {\n\t\t\tselectedGitHubCodingAgent = agentName\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes, copilot, codex, claude)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\tvar err error\n\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Printf(\"warning: failed to get GitHub context: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tgithubContext = \"\"\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\tif selectedGitHubCodingAgent != \"\" {\n\t\t\treturn runGitHubCodingAgentQuery(ctx, selectedGitHubCodingAgent, githubCodingAgentModel, question, awsContext, combinedCodeContext, githubContext)\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tcase \"github-models\":\n\t\t\t\tapiKey = \"\"\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().String(\"github-model\", \"\", \"GitHub Models model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes, copilot, codex, claude)\")\n\taskCmd.Flags().String(\"github-coding-agent-model\", \"\", \"Override the Copilot CLI model used for GitHub coding-agent delegation\")\n}\n\nfunc isGitHubCodingAgent(agentName string) bool {\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"copilot\", \"codex\", \"claude\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc runGitHubCodingAgentQuery(ctx context.Context, agentName, modelOverride, question, awsContext, codeContext, githubContext string) error {\n\tcopilotPath, err := exec.LookPath(\"copilot\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copilot cli not found in PATH: %w\", err)\n\t}\n\n\tprompt := buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext)\n\targs := []string{\"-p\", prompt, \"-s\", \"--excluded-tools=write\", \"--deny-tool=shell\"}\n\tif model := resolveGitHubCodingAgentModel(agentName, modelOverride); model != \"\" {\n\t\targs = append([]string{\"--model\", model}, args...)\n\t}\n\n\tcmd := exec.CommandContext(ctx, copilotPath, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %s via copilot cli: %w\", strings.TrimSpace(agentName), err)\n\t}\n\treturn nil\n}\n\nfunc resolveGitHubCodingAgentModel(agentName, modelOverride string) string {\n\tif trimmed := strings.TrimSpace(modelOverride); trimmed != \"\" {\n\t\treturn trimmed\n\t}\n\tswitch strings.TrimSpace(strings.ToLower(agentName)) {\n\tcase \"codex\":\n\t\treturn \"gpt-5.4\"\n\tcase \"claude\":\n\t\treturn \"claude-sonnet-4.6\"\n\tcase \"copilot\":\n\t\treturn \"gpt-5.4\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildGitHubCodingAgentPrompt(question, awsContext, codeContext, githubContext string) string {\n\tsections := []string{\n\t\t\"You are answering a Clanker infrastructure chat request.\",\n\t\t\"Use the provided Clanker-generated context when it is relevant. Answer directly and do not ask to modify files or run shell commands.\",\n\t}\n\n\tif trimmed := strings.TrimSpace(awsContext); trimmed != \"\" {\n\t\tsections = append(sections, \"AWS Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(codeContext); trimmed != \"\" {\n\t\tsections = append(sections, \"Additional Context:\\n\"+trimmed)\n\t}\n\tif trimmed := strings.TrimSpace(githubContext); trimmed != \"\" {\n\t\tsections = append(sections, \"GitHub Context:\\n\"+trimmed)\n\t}\n\n\tsections = append(sections, \"User Question:\\n\"+strings.TrimSpace(question))\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel, githubModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\tcase \"github-models\":\n\t\tif strings.TrimSpace(githubModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.github-models.model\", strings.TrimSpace(githubModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "a0e7079a445890e1d45c730791b4a1eebb977e18": {"t": 1785748569.5290687, "n": 89008, "h": "a65691e1b042ee40", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hermes\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle explicit --agent flag: delegate to a specific agent\n\t\tagentName, _ := cmd.Flags().GetString(\"agent\")\n\t\tif agentName == \"hermes\" {\n\t\t\treturn handleHermesQuery(context.Background(), question, profile, debug)\n\t\t} else if agentName != \"\" {\n\t\t\treturn fmt.Errorf(\"unknown agent: %s (available: hermes)\", agentName)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\n\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\t\tvar err error\n\t\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get GitHub context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n\taskCmd.Flags().String(\"agent\", \"\", \"Use a specific agent to handle the query (e.g., hermes)\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for explicit Hermes agent requests\n\thermesKeywords := []string{\"hermes\", \"hermes agent\", \"talk to hermes\", \"use hermes\"}\n\tfor _, kw := range hermesKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"hermes\", \"Hermes agent explicitly requested\"\n\t\t}\n\t}\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n\n// handleHermesQuery delegates a question to the Hermes agent and prints the response.\n// When an AWS profile is available, it gathers infrastructure context first so the\n// agent can answer questions about the user's environment.\nfunc handleHermesQuery(ctx context.Context, question string, profile string, debug bool) error {\n\thermesPath, err := hermes.FindHermesPath()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent not found: %w\\nRun 'make setup-hermes' to install\", err)\n\t}\n\n\trunner := hermes.NewRunner(hermesPath, debug)\n\trunner.SetEnv(buildHermesEnv())\n\n\tif err := runner.Start(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to start hermes agent: %w\", err)\n\t}\n\tdefer runner.Stop()\n\n\t// Gather AWS infrastructure context if a profile is available.\n\tprompt := question\n\ttargetProfile := profile\n\tif targetProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif targetProfile == \"\" {\n\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t}\n\n\tif targetProfile != \"\" {\n\t\tif debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] gathering AWS context with profile %s\\n\", targetProfile)\n\t\t}\n\t\tawsClient, err := aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\tif err == nil {\n\t\t\tawsContext, err := awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err == nil && strings.TrimSpace(awsContext) != \"\" {\n\t\t\t\tprompt = fmt.Sprintf(\"Here is the current AWS infrastructure context:\\n\\n%s\\n\\nUser question: %s\", awsContext, question)\n\t\t\t} else if debug && err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to get AWS context: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Fprintf(os.Stderr, \"[hermes] warning: failed to create AWS client: %v\\n\", err)\n\t\t}\n\t}\n\n\tresponse, err := runner.PromptSync(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hermes agent error: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// buildHermesEnv resolves clanker's API keys, AI provider, and hermes config\n// into environment variables for the bridge subprocess.\nfunc buildHermesEnv() []string {\n\tvar env []string\n\n\t// Determine the AI provider so the bridge knows which backend to use.\n\tprovider := viper.GetString(\"ai.default_provider\")\n\tif provider == \"\" {\n\t\tprovider = \"openai\"\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\":\n\t\tenv = append(env, \"HERMES_PROVIDER=bedrock\")\n\t\tif p := viper.GetString(\"ai.providers.bedrock.aws_profile\"); p != \"\" {\n\t\t\tenv = append(env, \"AWS_PROFILE=\"+p)\n\t\t}\n\t\tif r := viper.GetString(\"ai.providers.bedrock.region\"); r != \"\" {\n\t\t\tenv = append(env, \"AWS_REGION=\"+r)\n\t\t}\n\t\tif m := viper.GetString(\"ai.providers.bedrock.model\"); m != \"\" {\n\t\t\tenv = append(env, \"HERMES_BEDROCK_MODEL=\"+m)\n\t\t}\n\tcase \"anthropic\":\n\t\tenv = append(env, \"HERMES_PROVIDER=anthropic\")\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\tcase \"openai\":\n\t\tenv = append(env, \"HERMES_PROVIDER=openai\")\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\tdefault:\n\t\t// For other providers, pass through all available keys.\n\t\tif key := resolveOpenAIKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"OPENAI_API_KEY=\"+key)\n\t\t}\n\t\tif key := resolveAnthropicKey(\"\"); key != \"\" {\n\t\t\tenv = append(env, \"ANTHROPIC_API_KEY=\"+key)\n\t\t}\n\t}\n\n\tif key := resolveGeminiAPIKey(\"\"); key != \"\" {\n\t\tenv = append(env, \"GEMINI_API_KEY=\"+key)\n\t}\n\n\t// OpenRouter key from config or env\n\tif key := viper.GetString(\"hermes.openrouter_api_key\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t} else if key := os.Getenv(\"OPENROUTER_API_KEY\"); key != \"\" {\n\t\tenv = append(env, \"OPENROUTER_API_KEY=\"+key)\n\t}\n\n\t// Hermes model and base URL overrides from hermes config section\n\tif model := viper.GetString(\"hermes.model\"); model != \"\" {\n\t\tenv = append(env, \"HERMES_MODEL=\"+model)\n\t}\n\tif baseURL := viper.GetString(\"hermes.base_url\"); baseURL != \"\" {\n\t\tenv = append(env, \"HERMES_BASE_URL=\"+baseURL)\n\t}\n\n\treturn env\n}\n"}, "350625344ff2b7a59f71acad6a6650acc9bb40c8": {"t": 1785748570.9230626, "n": 84208, "h": "34e542c7340eea86", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tcohereKey, _ := cmd.Flags().GetString(\"cohere-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tcohereModel, _ := cmd.Flags().GetString(\"cohere-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\n\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\t\tvar err error\n\t\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get GitHub context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := resolveAWSProfile(profile)\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"cohere\":\n\t\t\t\tapiKey = resolveCohereKey(cohereKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"cohere-key\", \"\", \"Cohere API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"cohere-model\", \"\", \"Cohere model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveCohereKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.cohere.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.cohere.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"COHERE_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, cohereModel, minimaxModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"cohere\":\n\t\tif strings.TrimSpace(cohereModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.cohere.model\", strings.TrimSpace(cohereModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"cohere\":\n\t\tapiKey = resolveCohereKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := resolveAWSProfile(profile)\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n"}, "def065c60af9f5b4728186cd212427db0d1f02dd": {"t": 1785748572.4337757, "n": 84977, "h": "a5d62041fcd56ecf", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\tcheckCtx, checkCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\tcheckErr := maker.ValidateDigitalOceanAccess(checkCtx, doToken, os.Stderr)\n\t\t\t\tcheckCancel()\n\t\t\t\tif checkErr != nil {\n\t\t\t\t\treturn checkErr\n\t\t\t\t}\n\t\t\t\tif maker.PlanNeedsDigitalOceanRegistryPush(makerPlan) {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"[maker] prereq: probing DigitalOcean registry push access before apply...\")\n\t\t\t\t\tprobeCtx, probeCancel := context.WithTimeout(ctx, 3*time.Minute)\n\t\t\t\t\tprobeErr := maker.PrepareDigitalOceanRegistryPushPlan(probeCtx, doToken, makerPlan, os.Stdout)\n\t\t\t\t\tprobeCancel()\n\t\t\t\t\tif probeErr != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"[maker] warning: DigitalOcean registry prereq failed before apply; continuing and deferring exact registry handling to execution: %v\\n\", probeErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := profile\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t// Try infra config first\n\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t}\n\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t}\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\n\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\t\tvar err error\n\t\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get GitHub context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := profile\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t// Try infra config first\n\t\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := profile\n\tif awsProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = \"default\"\n\t\t}\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n"}, "74e2c10bec57052b47db5d9145b57e12cc43edd0": {"t": 1785748574.8285396, "n": 84187, "h": "0bba03dea9de2928", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/resourcedb\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Initialize resource tracking\n\t\t\tresourceStore, rsErr := resourcedb.NewStore(\"\")\n\t\t\tif rsErr != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"[ask] warning: resource tracking unavailable: %v\\n\", rsErr)\n\t\t\t}\n\t\t\tif resourceStore != nil {\n\t\t\t\tdefer resourceStore.Close()\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t\tResourceStore: resourceStore,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := profile\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t// Try infra config first\n\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t}\n\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t}\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\n\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\t\tvar err error\n\t\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get GitHub context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := profile\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t// Try infra config first\n\t\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := profile\n\tif awsProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = \"default\"\n\t\t}\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n"}, "3d75c573a40f8657628f8f4f70c52aabe9a920e5": {"t": 1785748576.240483, "n": 83810, "h": "073e3e01d6ce175a", "c": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bgdnvk/clanker/internal/ai\"\n\t\"github.com/bgdnvk/clanker/internal/aws\"\n\t\"github.com/bgdnvk/clanker/internal/azure\"\n\t\"github.com/bgdnvk/clanker/internal/backend\"\n\t\"github.com/bgdnvk/clanker/internal/cloudflare\"\n\tcfanalytics \"github.com/bgdnvk/clanker/internal/cloudflare/analytics\"\n\tcfdns \"github.com/bgdnvk/clanker/internal/cloudflare/dns\"\n\tcfwaf \"github.com/bgdnvk/clanker/internal/cloudflare/waf\"\n\tcfworkers \"github.com/bgdnvk/clanker/internal/cloudflare/workers\"\n\tcfzerotrust \"github.com/bgdnvk/clanker/internal/cloudflare/zerotrust\"\n\t\"github.com/bgdnvk/clanker/internal/digitalocean\"\n\t\"github.com/bgdnvk/clanker/internal/gcp\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/bgdnvk/clanker/internal/hetzner\"\n\tiamclient \"github.com/bgdnvk/clanker/internal/iam\"\n\t\"github.com/bgdnvk/clanker/internal/k8s\"\n\t\"github.com/bgdnvk/clanker/internal/k8s/plan\"\n\t\"github.com/bgdnvk/clanker/internal/maker\"\n\t\"github.com/bgdnvk/clanker/internal/routing\"\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// askCmd represents the ask command\nconst defaultGeminiModel = \"gemini-2.5-flash\"\n\nvar askCmd = &cobra.Command{\n\tUse: \"ask [question]\",\n\tShort: \"Ask AI about your cloud infrastructure or GitHub repository\",\n\tLong: `Ask natural language questions about your AWS or GCP infrastructure or GitHub repository.\n\t\nExamples:\n clanker ask \"What EC2 instances are running?\"\n clanker ask --gcp \"List Cloud Run services\"\n clanker ask \"Show me lambda functions with high error rates\"\n clanker ask \"What's the current RDS instance status?\"\n clanker ask \"Show me GitHub Actions workflow status\"\n clanker ask \"What pull requests are open?\"`,\n\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\tapply, _ := cmd.Flags().GetBool(\"apply\")\n\t\tif apply {\n\t\t\treturn nil\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t}\n\t\treturn nil\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tquestion := \"\"\n\t\tif len(args) > 0 {\n\t\t\tquestion = args[0]\n\t\t}\n\n\t\t// Get context from flags\n\t\tincludeAWS, _ := cmd.Flags().GetBool(\"aws\")\n\t\tincludeGitHub, _ := cmd.Flags().GetBool(\"github\")\n\t\tincludeGCP, _ := cmd.Flags().GetBool(\"gcp\")\n\t\tincludeAzure, _ := cmd.Flags().GetBool(\"azure\")\n\t\tincludeCloudflare, _ := cmd.Flags().GetBool(\"cloudflare\")\n\t\tincludeDigitalOcean, _ := cmd.Flags().GetBool(\"digitalocean\")\n\t\tincludeHetzner, _ := cmd.Flags().GetBool(\"hetzner\")\n\t\tincludeTerraform, _ := cmd.Flags().GetBool(\"terraform\")\n\t\tincludeIAM, _ := cmd.Flags().GetBool(\"iam\")\n\t\tiamRoleARN, _ := cmd.Flags().GetString(\"role-arn\")\n\t\tiamPolicyARN, _ := cmd.Flags().GetString(\"policy-arn\")\n\t\tdebug := viper.GetBool(\"debug\")\n\t\tdiscovery, _ := cmd.Flags().GetBool(\"discovery\")\n\t\tcompliance, _ := cmd.Flags().GetBool(\"compliance\")\n\t\tprofile, _ := cmd.Flags().GetString(\"profile\")\n\t\tworkspace, _ := cmd.Flags().GetString(\"workspace\")\n\t\tgcpProject, _ := cmd.Flags().GetString(\"gcp-project\")\n\t\tazureSubscription, _ := cmd.Flags().GetString(\"azure-subscription\")\n\t\taiProfile, _ := cmd.Flags().GetString(\"ai-profile\")\n\t\topenaiKey, _ := cmd.Flags().GetString(\"openai-key\")\n\t\tanthropicKey, _ := cmd.Flags().GetString(\"anthropic-key\")\n\t\tgeminiKey, _ := cmd.Flags().GetString(\"gemini-key\")\n\t\tdeepseekKey, _ := cmd.Flags().GetString(\"deepseek-key\")\n\t\tminimaxKey, _ := cmd.Flags().GetString(\"minimax-key\")\n\t\tgeminiModel, _ := cmd.Flags().GetString(\"gemini-model\")\n\t\topenaiModel, _ := cmd.Flags().GetString(\"openai-model\")\n\t\tanthropicModel, _ := cmd.Flags().GetString(\"anthropic-model\")\n\t\tdeepseekModel, _ := cmd.Flags().GetString(\"deepseek-model\")\n\t\tminimaxModel, _ := cmd.Flags().GetString(\"minimax-model\")\n\t\tmakerMode, _ := cmd.Flags().GetBool(\"maker\")\n\t\tapplyMode, _ := cmd.Flags().GetBool(\"apply\")\n\t\tplanFile, _ := cmd.Flags().GetString(\"plan-file\")\n\t\tdestroyer, _ := cmd.Flags().GetBool(\"destroyer\")\n\t\tagentTrace, _ := cmd.Flags().GetBool(\"agent-trace\")\n\t\tif cmd.Flags().Changed(\"agent-trace\") {\n\t\t\tviper.Set(\"agent.trace\", agentTrace)\n\t\t}\n\t\trouteOnly, _ := cmd.Flags().GetBool(\"route-only\")\n\n\t\t// Handle route-only mode: return routing decision as JSON without executing\n\t\tif routeOnly {\n\t\t\tagent, reason := determineRoutingDecision(question)\n\t\t\tresult := map[string]string{\n\t\t\t\t\"agent\": agent,\n\t\t\t\t\"reason\": reason,\n\t\t\t}\n\t\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t\t}\n\n\t\t// Handle apply mode (independent of maker mode)\n\t\tif applyMode {\n\t\t\tctx := context.Background()\n\t\t\tvar rawPlan string\n\t\t\tif planFile != \"\" {\n\t\t\t\tdata, err := os.ReadFile(planFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan file: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t} else {\n\t\t\t\tdata, err := io.ReadAll(os.Stdin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read plan from stdin: %w\", err)\n\t\t\t\t}\n\t\t\t\trawPlan = string(data)\n\t\t\t}\n\n\t\t\t// Check if this is a K8s plan (contains helm, eksctl, kubectl, or kubeadm commands)\n\t\t\tif isK8sPlan(rawPlan) {\n\t\t\t\treturn executeK8sPlan(ctx, rawPlan, profile, debug)\n\t\t\t}\n\n\t\t\t// Fall back to maker plan execution\n\t\t\tmakerPlan, err := maker.ParsePlan(rawPlan)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid plan: %w\", err)\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"gcp\") {\n\t\t\t\treturn maker.ExecuteGCPPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tGCPProject: gcpProject,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"azure\") {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteAzurePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tAzureSubscriptionID: sub,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"cloudflare\") {\n\t\t\t\tcfToken := cloudflare.ResolveAPIToken()\n\t\t\t\tcfAccountID := cloudflare.ResolveAccountID()\n\t\t\t\tif cfToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteCloudflarePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tCloudflareAPIToken: cfToken,\n\t\t\t\t\tCloudflareAccountID: cfAccountID,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"digitalocean\") {\n\t\t\t\tdoToken := digitalocean.ResolveAPIToken()\n\t\t\t\tif doToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteDigitalOceanPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tDigitalOceanAPIToken: doToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif strings.EqualFold(strings.TrimSpace(makerPlan.Provider), \"hetzner\") {\n\t\t\t\thetznerToken := hetzner.ResolveAPIToken()\n\t\t\t\tif hetznerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t\t\t\t}\n\t\t\t\treturn maker.ExecuteHetznerPlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\t\tHetznerAPIToken: hetznerToken,\n\t\t\t\t\tWriter: os.Stdout,\n\t\t\t\t\tDestroyer: destroyer,\n\t\t\t\t\tDebug: debug,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for execution.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\t// Prefer the profile's configured region so maker apply and infra analysis query the same region.\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t// Resolve provider for AI-assisted error handling\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\treturn maker.ExecutePlan(ctx, makerPlan, maker.ExecOptions{\n\t\t\t\tProfile: targetProfile,\n\t\t\t\tRegion: region,\n\t\t\t\tGCPProject: gcpProject,\n\t\t\t\tWriter: os.Stdout,\n\t\t\t\tDestroyer: destroyer,\n\t\t\t\tAIProvider: provider,\n\t\t\t\tAIAPIKey: apiKey,\n\t\t\t\tAIProfile: aiProfile,\n\t\t\t\tDebug: debug,\n\t\t\t})\n\t\t}\n\n\t\tif makerMode {\n\t\t\tctx := context.Background()\n\n\t\t\t// Resolve provider the same way as normal ask.\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Resolve API key based on provider.\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\t// Generate maker plan\n\t\t\tif strings.TrimSpace(question) == \"\" {\n\t\t\t\treturn fmt.Errorf(\"requires a question\")\n\t\t\t}\n\n\t\t\t// Decide provider for maker plans.\n\t\t\t// Priority:\n\t\t\t// 1) Explicit flags win (--gcp / --aws / --cloudflare)\n\t\t\t// 2) Infer from question (cheap heuristic)\n\t\t\tmakerProvider := \"aws\"\n\t\t\tmakerProviderReason := \"default\"\n\t\t\texplicitGCP := cmd.Flags().Changed(\"gcp\") && includeGCP\n\t\t\texplicitAWS := cmd.Flags().Changed(\"aws\") && includeAWS\n\t\t\texplicitCloudflare := cmd.Flags().Changed(\"cloudflare\") && includeCloudflare\n\t\t\texplicitDigitalOcean := cmd.Flags().Changed(\"digitalocean\") && includeDigitalOcean\n\t\t\texplicitHetzner := cmd.Flags().Changed(\"hetzner\") && includeHetzner\n\t\t\texplicitAzure := cmd.Flags().Changed(\"azure\") && includeAzure\n\t\t\texplicitCount := 0\n\t\t\tif explicitGCP {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAWS {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCloudflare {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitDigitalOcean {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitHetzner {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitAzure {\n\t\t\t\texplicitCount++\n\t\t\t}\n\t\t\tif explicitCount > 1 {\n\t\t\t\treturn fmt.Errorf(\"cannot use multiple provider flags (--aws, --gcp, --azure, --cloudflare, --digitalocean, --hetzner) together with --maker\")\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase explicitHetzner:\n\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitCloudflare:\n\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitDigitalOcean:\n\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAzure:\n\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitGCP:\n\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tcase explicitAWS:\n\t\t\t\tmakerProvider = \"aws\"\n\t\t\t\tmakerProviderReason = \"explicit\"\n\t\t\tdefault:\n\t\t\t\tsvcCtx := routing.InferContext(questionForRouting(question))\n\t\t\t\tif svcCtx.Cloudflare {\n\t\t\t\t\tmakerProvider = \"cloudflare\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.DigitalOcean {\n\t\t\t\t\tmakerProvider = \"digitalocean\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Hetzner {\n\t\t\t\t\tmakerProvider = \"hetzner\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.Azure {\n\t\t\t\t\tmakerProvider = \"azure\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t} else if svcCtx.GCP {\n\t\t\t\t\tmakerProvider = \"gcp\"\n\t\t\t\t\tmakerProviderReason = \"inferred\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log to stderr so stdout stays valid JSON.\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"[maker] provider=%s (%s)\\n\", makerProvider, makerProviderReason)\n\n\t\t\taiClient := ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t\tvar prompt string\n\t\t\tswitch makerProvider {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tprompt = maker.CloudflarePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"digitalocean\":\n\t\t\t\tprompt = maker.DigitalOceanPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"hetzner\":\n\t\t\t\tprompt = maker.HetznerPlanPromptWithMode(question, destroyer)\n\t\t\tcase \"azure\":\n\t\t\t\tprompt = maker.AzurePlanPromptWithMode(question, destroyer)\n\t\t\tcase \"gcp\":\n\t\t\t\tprompt = maker.GCPPlanPromptWithMode(question, destroyer)\n\t\t\tdefault:\n\t\t\t\tprompt = maker.PlanPromptWithMode(question, destroyer)\n\t\t\t}\n\n\t\t\tconst maxMakerPlanAttempts = 4\n\t\t\tvar lastParseErr error\n\t\t\tvar plan *maker.Plan\n\t\t\tfor attempt := 1; attempt <= maxMakerPlanAttempts; attempt++ {\n\t\t\t\tattemptPrompt := prompt\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\textra := \"Regenerate a VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\tif lastParseErr != nil {\n\t\t\t\t\t\terrText := strings.ToLower(lastParseErr.Error())\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.Contains(errText, \"plan has no commands\") || strings.Contains(errText, \"no commands\"):\n\t\t\t\t\t\t\textra = \"Your previous output had an empty (or missing) commands array. Regenerate a VALID JSON plan with commands. If the user request is ambiguous or missing required details, output a DISCOVERY-ONLY plan with at least 3 READ-ONLY commands that gather the missing inputs (still a non-empty commands array).\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"empty plan\") || strings.Contains(errText, \"unexpected end of json\") || strings.Contains(errText, \"unexpected eof\"):\n\t\t\t\t\t\t\textra = \"Your previous output was empty or truncated. Regenerate a COMPLETE, VALID JSON plan that matches the schema exactly and includes a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"invalid character\") || strings.Contains(errText, \"cannot unmarshal\") || strings.Contains(errText, \"json:\"):\n\t\t\t\t\t\t\textra = \"Your previous output was not valid JSON. Output ONLY a single JSON object matching the schema exactly (no code fences, no markdown, no backticks, no commentary). Include a NON-EMPTY commands array.\"\n\t\t\t\t\t\tcase strings.Contains(errText, \"has empty args\") || strings.Contains(errText, \"empty args\"):\n\t\t\t\t\t\t\textra = \"One of your commands had empty args. Ensure every command has a non-empty args array and args are individual tokens (no single-string commands).\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tattemptPrompt = fmt.Sprintf(\n\t\t\t\t\t\t\"%s\\n\\nIMPORTANT: Your previous output was invalid (%v). %s\\n\\nOutput rules (STRICT):\\n- Output ONLY JSON (no markdown, no prose).\\n- Do NOT wrap in ``` or any other code fences.\\n- The response MUST start with '{' and end with '}'.\\n- Include required fields: version, createdAt (RFC3339), provider, question, summary, commands.\\n- commands MUST be non-empty.\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\tlastParseErr,\n\t\t\t\t\t\textra,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tresp, err := aiClient.AskPrompt(ctx, attemptPrompt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcleaned := aiClient.CleanJSONResponse(resp)\n\t\t\t\ttrimmed := strings.TrimSpace(cleaned)\n\t\t\t\tif trimmed == \"\" {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"empty plan\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(trimmed, \"{\") || !strings.HasSuffix(trimmed, \"}\") {\n\t\t\t\t\tlastParseErr = fmt.Errorf(\"invalid json: response must be a single JSON object\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparsed, err := maker.ParsePlan(trimmed)\n\t\t\t\tif err == nil {\n\t\t\t\t\tplan = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlastParseErr = err\n\t\t\t}\n\t\t\tif plan == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse maker plan: %w\", lastParseErr)\n\t\t\t}\n\n\t\t\tplan.Provider = makerProvider\n\n\t\t\t// Handle GCP, Azure, Cloudflare, and Digital Ocean plans (output directly, no enrichment)\n\t\t\tproviderLower := strings.ToLower(strings.TrimSpace(plan.Provider))\n\t\t\tif providerLower == \"gcp\" || providerLower == \"azure\" || providerLower == \"cloudflare\" || providerLower == \"digitalocean\" || providerLower == \"hetzner\" {\n\t\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t\t}\n\t\t\t\tplan.Question = question\n\t\t\t\tif plan.Version == 0 {\n\t\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t\t}\n\t\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(string(out))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Resolve AWS profile/region for planning-time dependency expansion.\n\t\t\ttargetProfile := profile\n\t\t\tif targetProfile == \"\" {\n\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t}\n\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t}\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\ttargetProfile = \"default\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregion := \"\"\n\t\t\tif envRegion := strings.TrimSpace(os.Getenv(\"AWS_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else if envRegion := strings.TrimSpace(os.Getenv(\"AWS_DEFAULT_REGION\")); envRegion != \"\" {\n\t\t\t\tregion = envRegion\n\t\t\t} else {\n\t\t\t\tcmd := exec.CommandContext(ctx, \"aws\", \"configure\", \"get\", \"region\", \"--profile\", targetProfile)\n\t\t\t\tif out, err := cmd.CombinedOutput(); err == nil {\n\t\t\t\t\tregion = strings.TrimSpace(string(out))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = ai.FindInfraAnalysisRegion()\n\t\t\t}\n\t\t\tif region == \"\" {\n\t\t\t\tregion = \"us-east-1\"\n\t\t\t}\n\n\t\t\t_ = maker.EnrichPlan(ctx, plan, maker.ExecOptions{Profile: targetProfile, Region: region, Writer: io.Discard, Destroyer: destroyer})\n\n\t\t\tif plan.CreatedAt.IsZero() {\n\t\t\t\tplan.CreatedAt = time.Now().UTC()\n\t\t\t}\n\t\t\tplan.Question = question\n\t\t\tif plan.Version == 0 {\n\t\t\t\tplan.Version = maker.CurrentPlanVersion\n\t\t\t}\n\n\t\t\tout, err := json.MarshalIndent(plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(out))\n\t\t\treturn nil\n\t\t}\n\n\t\t// Compliance mode enables comprehensive service discovery with specific formatting\n\t\tif compliance {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tdiscovery = true // Enable full discovery for comprehensive compliance data\n\t\t\tquestion = `Generate a comprehensive SSP (System Security Plan) compliance report \"Services, Ports, and Protocols\". \n\nCreate a detailed table with the following columns exactly as specified:\n- Reference # (sequential numbering)\n- System (service name)\n- Vendor (AWS, or specific vendor if applicable)\n- Port (specific port numbers used)\n- Protocol (TCP, UDP, HTTPS, etc.)\n- External IP Address (public IPs, DNS names, or \"Internal\" if private)\n- Description (detailed purpose and function)\n- Hosting Environment (AWS region, VPC, or specific environment details)\n- Risk/Impact/Mitigation (security measures, encryption, access controls)\n- Authorizing Official (system owner or responsible party)\n\nFor each active AWS service with resources, identify:\n1. The specific ports and protocols it uses\n2. Whether it has external access or is internal-only\n3. The security controls and mitigations in place\n4. The hosting environment details\n\nInclude all active services: compute, storage, database, networking, security, ML/AI, analytics, and management services. Focus on services that actually have active resources deployed.\n\nFormat as a professional compliance table suitable for government security documentation.`\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Compliance mode enabled: Full infrastructure discovery for comprehensive SSP documentation\")\n\t\t\t}\n\t\t}\n\n\t\t// Discovery mode enables comprehensive infrastructure analysis\n\t\tif discovery {\n\t\t\tincludeAWS = true\n\t\t\tincludeTerraform = true\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"Discovery mode enabled: AWS and Terraform contexts activated\")\n\t\t\t}\n\t\t}\n\n\t\t// If no specific context is requested, try to infer from the question\n\t\tif workspace != \"\" {\n\t\t\tincludeTerraform = true\n\t\t}\n\n\t\t// Handle explicit --cloudflare flag\n\t\tif includeCloudflare {\n\t\t\treturn handleCloudflareQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --digitalocean flag\n\t\tif includeDigitalOcean {\n\t\t\treturn handleDigitalOceanQuery(context.Background(), question, debug)\n\t\t}\n\n\t\t// Handle explicit --hetzner flag\n\t\tif includeHetzner {\n\t\t\treturn handleHetznerQuery(context.Background(), question, debug)\n\t\t}\n\n\t\tif !includeAWS && !includeGitHub && !includeTerraform && !includeGCP && !includeAzure && !includeCloudflare && !includeDigitalOcean && !includeHetzner {\n\t\t\troutingQuestion := questionForRouting(question)\n\n\t\t\t// First, do quick keyword check for explicit terms\n\t\t\tsvcCtx := routing.InferContext(routingQuestion)\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Keyword inference: AWS=%v, GitHub=%v, Terraform=%v, K8s=%v, GCP=%v, Cloudflare=%v\\n\",\n\t\t\t\t\tsvcCtx.AWS, svcCtx.GitHub, svcCtx.Terraform, svcCtx.K8s, svcCtx.GCP, svcCtx.Cloudflare)\n\t\t\t}\n\n\t\t\t// For ambiguous queries (multiple services detected or Cloudflare detected),\n\t\t\t// use LLM to make the final routing decision\n\t\t\tif routing.NeedsLLMClassification(svcCtx) {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"[routing] Ambiguous query detected, using LLM for classification...\")\n\t\t\t\t}\n\n\t\t\t\tllmService, err := routing.ClassifyWithLLM(context.Background(), routingQuestion, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// FALLBACK: LLM classification failed, use keyword-based inference\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"[routing] LLM classification failed (%v), falling back to keyword inference\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the keyword-inferred values as-is (no changes needed)\n\t\t\t\t} else {\n\t\t\t\t\t// LLM succeeded - override keyword-based inference with LLM decision\n\t\t\t\t\trouting.ApplyLLMClassification(&svcCtx, llmService)\n\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"LLM override: AWS=%v, K8s=%v, GCP=%v, Azure=%v, Cloudflare=%v\\n\",\n\t\t\t\t\t\t\tsvcCtx.AWS, svcCtx.K8s, svcCtx.GCP, svcCtx.Azure, svcCtx.Cloudflare)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle inferred Terraform context\n\t\t\tif svcCtx.Terraform {\n\t\t\t\tincludeTerraform = true\n\t\t\t}\n\n\t\t\tif svcCtx.GCP {\n\t\t\t\tincludeGCP = true\n\t\t\t}\n\n\t\t\tif svcCtx.Azure {\n\t\t\t\tincludeAzure = true\n\t\t\t}\n\n\t\t\t// Update includeAWS and includeGitHub from service context\n\t\t\tincludeAWS = svcCtx.AWS\n\t\t\tincludeGitHub = svcCtx.GitHub\n\t\t\tincludeAzure = svcCtx.Azure\n\n\t\t\t// Handle Cloudflare queries by delegating to Cloudflare agent\n\t\t\tif svcCtx.Cloudflare {\n\t\t\t\treturn handleCloudflareQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Digital Ocean queries\n\t\t\tif svcCtx.DigitalOcean {\n\t\t\t\treturn handleDigitalOceanQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle Hetzner queries\n\t\t\tif svcCtx.Hetzner {\n\t\t\t\treturn handleHetznerQuery(context.Background(), routingQuestion, debug)\n\t\t\t}\n\n\t\t\t// Handle IAM queries by delegating to IAM agent\n\t\t\tif includeIAM || svcCtx.IAM {\n\t\t\t\treturn handleIAMQuery(context.Background(), routingQuestion, debug, iamRoleARN, iamPolicyARN)\n\t\t\t}\n\n\t\t\t// Handle K8s queries by delegating to K8s agent\n\t\t\tif svcCtx.K8s {\n\t\t\t\treturn handleK8sQuery(context.Background(), routingQuestion, debug, viper.GetString(\"kubernetes.kubeconfig\"))\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\t// Gather context\n\t\tvar awsContext string\n\t\tvar githubContext string\n\t\tvar terraformContext string\n\t\tvar gcpContext string\n\t\tvar azureContext string\n\n\t\tif includeAWS {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar err error\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\tif backendErr == nil {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\tif awsClient == nil {\n\t\t\t\t// Use specified profile or default from config\n\t\t\t\ttargetProfile := profile\n\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t// Try infra config first\n\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t}\n\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t}\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawsContext, err = awsClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AWS context: %w\", err)\n\t\t\t}\n\n\t\t\tif discovery {\n\t\t\t\trolesContext, err := awsClient.GetRelevantContext(ctx, \"iam roles\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get AWS IAM roles context: %w\", err)\n\t\t\t\t}\n\t\t\t\tif strings.TrimSpace(rolesContext) != \"\" {\n\t\t\t\t\tawsContext = awsContext + rolesContext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGitHub {\n\t\t\t// Get GitHub configuration\n\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\trepo := viper.GetString(\"github.repo\")\n\n\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\tgithubClient := ghclient.NewClient(token, owner, repo)\n\t\t\t\tvar err error\n\t\t\t\tgithubContext, err = githubClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get GitHub context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeTerraform {\n\t\t\tworkspaces := viper.GetStringMap(\"terraform.workspaces\")\n\t\t\tif workspace == \"\" && len(workspaces) == 0 {\n\t\t\t\tif debug {\n\t\t\t\t\tfmt.Println(\"Terraform context requested but no workspaces configured, skipping\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttfClient, err := tfclient.NewClient(workspace)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Terraform client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tran, err := maybeRunTerraformCommand(ctx, question, tfClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif ran {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tterraformContext, err = tfClient.GetRelevantContext(ctx, question)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get Terraform context: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif includeGCP {\n\t\t\tvar gcpClient *gcp.Client\n\t\t\tvar gcpCredsFile string\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetGCPCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.ProjectID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using GCP credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tgcpClient, gcpCredsFile, err = gcp.NewClientWithCredentials(&gcp.BackendGCPCredentials{\n\t\t\t\t\t\tProjectID: backendCreds.ProjectID,\n\t\t\t\t\t\tServiceAccountJSON: backendCreds.ServiceAccountJSON,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif gcpCredsFile != \"\" {\n\t\t\t\t\t\tdefer gcp.CleanupCredentialsFile(gcpCredsFile)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No GCP credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif gcpClient == nil {\n\t\t\t\tprojectID := gcpProject\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\tprojectID = gcp.ResolveProjectID()\n\t\t\t\t}\n\t\t\t\tif projectID == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"gcp project_id is required (set infra.gcp.project_id or use --gcp-project)\")\n\t\t\t\t}\n\n\t\t\t\tvar err error\n\t\t\t\tgcpClient, err = gcp.NewClient(projectID, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create GCP client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tgcpContext, err = gcpClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get GCP context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif includeAzure {\n\t\t\tvar azureClient *azure.Client\n\n\t\t\t// Check for backend API key first\n\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t}\n\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t// Try to get credentials from backend\n\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\tbackendCreds, backendErr := backendClient.GetAzureCredentials(ctx)\n\t\t\t\tif backendErr == nil && backendCreds.SubscriptionID != \"\" {\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Println(\"[backend] Using Azure credentials from backend\")\n\t\t\t\t\t}\n\t\t\t\t\tvar err error\n\t\t\t\t\tazureClient, err = azure.NewClientWithCredentials(&azure.BackendAzureCredentials{\n\t\t\t\t\t\tSubscriptionID: backendCreds.SubscriptionID,\n\t\t\t\t\t\tTenantID: backendCreds.TenantID,\n\t\t\t\t\t\tClientID: backendCreds.ClientID,\n\t\t\t\t\t\tClientSecret: backendCreds.ClientSecret,\n\t\t\t\t\t}, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client with backend credentials: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if debug {\n\t\t\t\t\tfmt.Printf(\"[backend] No Azure credentials available (%v), falling back to local\\n\", backendErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to local config if backend credentials not available\n\t\t\tif azureClient == nil {\n\t\t\t\tsub := strings.TrimSpace(azureSubscription)\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\tsub = azure.ResolveSubscriptionID()\n\t\t\t\t}\n\t\t\t\tif sub == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"azure subscription_id is required (set infra.azure.subscription_id, AZURE_SUBSCRIPTION_ID, or use --azure-subscription)\")\n\t\t\t\t}\n\t\t\t\tvar err error\n\t\t\t\tazureClient, err = azure.NewClient(sub, debug)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create Azure client: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tazureContext, err = azureClient.GetRelevantContext(ctx, question)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get Azure context: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Query AI with tool support\n\t\tvar aiClient *ai.Client\n\t\tvar err error\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Tool calling check: includeAWS=%v, includeGitHub=%v\\n\", includeAWS, includeGitHub)\n\t\t}\n\n\t\t// Create AI client with AWS and GitHub clients for tool calling\n\t\tif includeAWS || includeGitHub {\n\t\t\tvar awsClient *aws.Client\n\t\t\tvar githubClient *ghclient.Client\n\n\t\t\tif includeAWS {\n\t\t\t\t// Check for backend API key first\n\t\t\t\tapiKeyFlag, _ := cmd.Flags().GetString(\"api-key\")\n\t\t\t\tif apiKeyFlag == \"\" {\n\t\t\t\t\tapiKeyFlag, _ = cmd.Root().PersistentFlags().GetString(\"api-key\")\n\t\t\t\t}\n\t\t\t\tbackendAPIKey := backend.ResolveAPIKey(apiKeyFlag)\n\n\t\t\t\tif backendAPIKey != \"\" {\n\t\t\t\t\t// Try to get credentials from backend\n\t\t\t\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\t\t\t\tbackendCreds, backendErr := backendClient.GetAWSCredentials(ctx)\n\t\t\t\t\tif backendErr == nil {\n\t\t\t\t\t\tif debug {\n\t\t\t\t\t\t\tfmt.Println(\"[backend] Using AWS credentials from backend for tool calling\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawsClient, err = aws.NewClientWithCredentials(ctx, &aws.BackendAWSCredentials{\n\t\t\t\t\t\t\tAccessKeyID: backendCreds.AccessKeyID,\n\t\t\t\t\t\t\tSecretAccessKey: backendCreds.SecretAccessKey,\n\t\t\t\t\t\t\tRegion: backendCreds.Region,\n\t\t\t\t\t\t\tSessionToken: backendCreds.SessionToken,\n\t\t\t\t\t\t}, debug)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with backend credentials: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if debug {\n\t\t\t\t\t\tfmt.Printf(\"[backend] No AWS credentials available for tools (%v), falling back to local\\n\", backendErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fall back to local profile if backend credentials not available\n\t\t\t\tif awsClient == nil {\n\t\t\t\t\t// Use specified profile or default from config\n\t\t\t\t\ttargetProfile := profile\n\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t// Try infra config first\n\t\t\t\t\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\t\t\t\t\tif defaultEnv == \"\" {\n\t\t\t\t\t\t\tdefaultEnv = \"dev\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttargetProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = viper.GetString(\"aws.default_profile\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif targetProfile == \"\" {\n\t\t\t\t\t\t\ttargetProfile = \"default\" // fallback\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tawsClient, err = aws.NewClientWithProfileAndDebug(ctx, targetProfile, debug)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to create AWS client with profile %s: %w\", targetProfile, err)\n\t\t\t\t\t}\n\t\t\t\t\tif debug {\n\t\t\t\t\t\tfmt.Printf(\"Successfully created AWS client with profile: %s\\n\", targetProfile)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif includeGitHub {\n\t\t\t\ttoken := viper.GetString(\"github.token\")\n\t\t\t\towner := viper.GetString(\"github.owner\")\n\t\t\t\trepo := viper.GetString(\"github.repo\")\n\t\t\t\tif owner != \"\" && repo != \"\" {\n\t\t\t\t\tgithubClient = ghclient.NewClient(token, owner, repo)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClientWithTools(provider, apiKey, awsClient, githubClient, debug, aiProfile)\n\t\t\tif debug {\n\t\t\t\tfmt.Printf(\"Created AI client with tools: AWS=%v, GitHub=%v\\n\", awsClient != nil, githubClient != nil)\n\t\t\t}\n\t\t} else {\n\t\t\t// Get the provider from the AI profile, or use default\n\t\t\tvar provider string\n\t\t\tif aiProfile != \"\" {\n\t\t\t\t// Use the specified AI profile name as the provider\n\t\t\t\tprovider = aiProfile\n\t\t\t} else {\n\t\t\t\t// Use the default provider from config\n\t\t\t\tprovider = viper.GetString(\"ai.default_provider\")\n\t\t\t\tif provider == \"\" {\n\t\t\t\t\tprovider = \"openai\" // fallback\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel)\n\n\t\t\t// Get the appropriate API key based on provider\n\t\t\tvar apiKey string\n\t\t\tswitch provider {\n\t\t\tcase \"gemini\":\n\t\t\t\t// Gemini uses Application Default Credentials - no API key needed\n\t\t\t\tapiKey = \"\"\n\t\t\tcase \"gemini-api\":\n\t\t\t\tapiKey = resolveGeminiAPIKey(geminiKey)\n\t\t\tcase \"openai\":\n\t\t\t\tapiKey = resolveOpenAIKey(openaiKey)\n\t\t\tcase \"anthropic\":\n\t\t\t\tapiKey = resolveAnthropicKey(anthropicKey)\n\t\t\tcase \"deepseek\":\n\t\t\t\tapiKey = resolveDeepSeekKey(deepseekKey)\n\t\t\tcase \"minimax\":\n\t\t\t\tapiKey = resolveMiniMaxKey(minimaxKey)\n\t\t\tdefault:\n\t\t\t\t// Default/other providers\n\t\t\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t\t\t}\n\n\t\t\taiClient = ai.NewClient(provider, apiKey, debug, aiProfile)\n\t\t}\n\n\t\t// Only Terraform context is supported here (code scanning disabled).\n\t\tcombinedCodeContext := terraformContext\n\t\tif strings.TrimSpace(gcpContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"GCP Context:\\n\" + gcpContext\n\t\t}\n\t\tif strings.TrimSpace(azureContext) != \"\" {\n\t\t\tif combinedCodeContext != \"\" {\n\t\t\t\tcombinedCodeContext += \"\\n\"\n\t\t\t}\n\t\t\tcombinedCodeContext += \"Azure Context:\\n\" + azureContext\n\t\t}\n\n\t\t// If no tools are enabled, skip the tool-calling pipeline entirely.\n\t\t// This avoids confusing \"selected operations\" output that cannot execute.\n\t\tif !includeAWS && !includeGitHub {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"No tools enabled (AWS/GitHub). Skipping tool pipeline.\")\n\t\t\t}\n\t\t\tresponse, err := aiClient.AskOriginal(ctx, question, awsContext, combinedCodeContext, githubContext)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(response)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use the same AWS profile for both infrastructure queries and tool calls\n\t\tawsProfileForTools := profile\n\t\tif awsProfileForTools == \"\" {\n\t\t\t// First try to get the profile from profile-infra-analysis configuration\n\t\t\tawsProfileForTools = ai.FindInfraAnalysisProfile()\n\t\t}\n\n\t\tif debug {\n\t\t\tfmt.Printf(\"Calling AskWithTools with AWS profile: %s\\n\", awsProfileForTools)\n\t\t}\n\n\t\tresponse, err := aiClient.AskWithTools(ctx, question, awsContext, combinedCodeContext, awsProfileForTools, githubContext)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t\t}\n\n\t\tfmt.Println(response)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(askCmd)\n\n\taskCmd.Flags().Bool(\"aws\", false, \"Include AWS infrastructure context\")\n\taskCmd.Flags().Bool(\"gcp\", false, \"Include GCP infrastructure context\")\n\taskCmd.Flags().Bool(\"azure\", false, \"Include Azure infrastructure context\")\n\taskCmd.Flags().Bool(\"cloudflare\", false, \"Include Cloudflare infrastructure context\")\n\taskCmd.Flags().Bool(\"digitalocean\", false, \"Include Digital Ocean infrastructure context\")\n\taskCmd.Flags().Bool(\"hetzner\", false, \"Include Hetzner Cloud infrastructure context\")\n\taskCmd.Flags().Bool(\"github\", false, \"Include GitHub repository context\")\n\taskCmd.Flags().Bool(\"terraform\", false, \"Include Terraform workspace context\")\n\taskCmd.Flags().Bool(\"iam\", false, \"Route query to IAM agent for security analysis\")\n\taskCmd.Flags().String(\"role-arn\", \"\", \"Scope IAM query to a specific role ARN\")\n\taskCmd.Flags().String(\"policy-arn\", \"\", \"Scope IAM query to a specific policy ARN\")\n\taskCmd.Flags().Bool(\"discovery\", false, \"Run comprehensive infrastructure discovery (all services)\")\n\taskCmd.Flags().Bool(\"compliance\", false, \"Generate compliance report showing all services, ports, and protocols\")\n\taskCmd.Flags().String(\"profile\", \"\", \"AWS profile to use for infrastructure queries\")\n\taskCmd.Flags().String(\"gcp-project\", \"\", \"GCP project ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"azure-subscription\", \"\", \"Azure subscription ID to use for infrastructure queries\")\n\taskCmd.Flags().String(\"workspace\", \"\", \"Terraform workspace to use for infrastructure queries\")\n\taskCmd.Flags().String(\"ai-profile\", \"\", \"AI profile to use (default: 'default')\")\n\taskCmd.Flags().String(\"openai-key\", \"\", \"OpenAI API key (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-key\", \"\", \"Anthropic API key (overrides config)\")\n\taskCmd.Flags().String(\"gemini-key\", \"\", \"Gemini API key (overrides config and env vars)\")\n\taskCmd.Flags().String(\"deepseek-key\", \"\", \"DeepSeek API key (overrides config)\")\n\taskCmd.Flags().String(\"minimax-key\", \"\", \"MiniMax API key (overrides config)\")\n\taskCmd.Flags().String(\"openai-model\", \"\", \"OpenAI model to use (overrides config)\")\n\taskCmd.Flags().String(\"anthropic-model\", \"\", \"Anthropic model to use (overrides config)\")\n\taskCmd.Flags().String(\"gemini-model\", \"\", \"Gemini model to use (overrides config)\")\n\taskCmd.Flags().String(\"deepseek-model\", \"\", \"DeepSeek model to use (overrides config)\")\n\taskCmd.Flags().String(\"minimax-model\", \"\", \"MiniMax model to use (overrides config)\")\n\taskCmd.Flags().Bool(\"agent-trace\", false, \"Show detailed coordinator agent lifecycle logs (overrides config)\")\n\taskCmd.Flags().Bool(\"maker\", false, \"Generate an AWS, GCP, Azure, Cloudflare, Digital Ocean, or Hetzner CLI plan (JSON) for infrastructure changes\")\n\taskCmd.Flags().Bool(\"destroyer\", false, \"Allow destructive operations when using --maker (requires explicit confirmation in UI/workflow)\")\n\taskCmd.Flags().Bool(\"apply\", false, \"Apply an approved maker plan (reads from stdin unless --plan-file is provided)\")\n\taskCmd.Flags().String(\"plan-file\", \"\", \"Optional path to maker plan JSON file for --apply\")\n\taskCmd.Flags().Bool(\"route-only\", false, \"Return routing decision as JSON without executing (for backend integration)\")\n}\n\nfunc resolveGeminiAPIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.gemini-api.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.gemini-api.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"GEMINI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveOpenAIKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveAnthropicKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.anthropic.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.anthropic.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"ANTHROPIC_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveDeepSeekKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.deepseek.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.deepseek.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"DEEPSEEK_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc resolveMiniMaxKey(flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\tif key := viper.GetString(\"ai.providers.minimax.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.minimax.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"MINIMAX_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc maybeRunTerraformCommand(ctx context.Context, question string, tfClient *tfclient.Client) (bool, error) {\n\tq := strings.ToLower(strings.TrimSpace(question))\n\tif q == \"\" {\n\t\treturn false, nil\n\t}\n\n\tisInit := strings.Contains(q, \"terraform init\") || strings.Contains(q, \"init terraform\")\n\tisPlan := strings.Contains(q, \"terraform plan\") || strings.Contains(q, \"plan terraform\")\n\tisApply := strings.Contains(q, \"terraform apply\") || strings.Contains(q, \"apply terraform\")\n\tapplyConfirmed := strings.Contains(q, \"confirm apply\") || strings.Contains(q, \"approved apply\") || strings.Contains(q, \"apply confirmed\")\n\n\tif !isInit && !isPlan && !isApply {\n\t\treturn false, nil\n\t}\n\n\tvar output string\n\tvar err error\n\tif isInit {\n\t\toutput, err = tfClient.RunInit(ctx)\n\t} else if isPlan {\n\t\toutput, err = tfClient.RunPlan(ctx)\n\t} else if isApply {\n\t\tif !applyConfirmed {\n\t\t\treturn true, fmt.Errorf(\"terraform apply requires confirmation: include 'confirm apply' in your request\")\n\t\t}\n\t\toutput, err = tfClient.RunApply(ctx)\n\t}\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif output != \"\" {\n\t\tfmt.Println(output)\n\t}\n\treturn true, nil\n}\n\nfunc maybeOverrideProviderModel(provider, openaiModel, anthropicModel, geminiModel, deepseekModel, minimaxModel string) {\n\tswitch provider {\n\tcase \"openai\":\n\t\tif strings.TrimSpace(openaiModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.openai.model\", strings.TrimSpace(openaiModel))\n\t\t}\n\tcase \"anthropic\":\n\t\tif strings.TrimSpace(anthropicModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.anthropic.model\", strings.TrimSpace(anthropicModel))\n\t\t}\n\tcase \"gemini\", \"gemini-api\":\n\t\tif model := resolveGeminiModel(provider, geminiModel); model != \"\" {\n\t\t\tviper.Set(fmt.Sprintf(\"ai.providers.%s.model\", provider), model)\n\t\t}\n\tcase \"deepseek\":\n\t\tif strings.TrimSpace(deepseekModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.deepseek.model\", strings.TrimSpace(deepseekModel))\n\t\t}\n\tcase \"minimax\":\n\t\tif strings.TrimSpace(minimaxModel) != \"\" {\n\t\t\tviper.Set(\"ai.providers.minimax.model\", strings.TrimSpace(minimaxModel))\n\t\t}\n\t}\n}\n\nfunc resolveGeminiModel(provider, flagValue string) string {\n\tif flagValue != \"\" {\n\t\treturn flagValue\n\t}\n\n\tconfigKey := fmt.Sprintf(\"ai.providers.%s.model\", provider)\n\tmodel := viper.GetString(configKey)\n\tif model == \"\" || strings.EqualFold(model, \"gemini-pro\") {\n\t\treturn defaultGeminiModel\n\t}\n\n\treturn model\n}\n\nfunc questionForRouting(question string) string {\n\ttrimmed := strings.TrimSpace(question)\n\tif trimmed == \"\" {\n\t\treturn trimmed\n\t}\n\n\t// If the prompt contains a chat transcript (as emitted by clanker-cloud),\n\t// route based on the last explicit user turn.\n\t// Format we expect (roughly):\n\t// You\\n\\n\\nClanker\\n...\n\tstart := strings.LastIndex(trimmed, \"\\nYou\\n\")\n\tstartLen := len(\"\\nYou\\n\")\n\tif start == -1 && strings.HasPrefix(trimmed, \"You\\n\") {\n\t\tstart = 0\n\t\tstartLen = len(\"You\\n\")\n\t}\n\n\tif start != -1 {\n\t\tcandidate := trimmed[start+startLen:]\n\t\t// End at next assistant turn marker if present.\n\t\tif end := strings.Index(candidate, \"\\n\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t} else if end := strings.Index(candidate, \"\\nClanker\\n\"); end != -1 {\n\t\t\tcandidate = candidate[:end]\n\t\t}\n\t\tcandidate = strings.TrimSpace(candidate)\n\t\tif candidate != \"\" {\n\t\t\treturn candidate\n\t\t}\n\t}\n\n\t// Generic fallback: if a prompt appends one or more sections like\n\t// \"Current context:\", route on the text before the first such section.\n\tlower := strings.ToLower(trimmed)\n\tif idx := strings.Index(lower, \"\\ncurrent \"); idx != -1 {\n\t\tif strings.Contains(lower[idx:], \" context:\") {\n\t\t\tbefore := strings.TrimSpace(trimmed[:idx])\n\t\t\tif before != \"\" {\n\t\t\t\treturn before\n\t\t\t}\n\t\t}\n\t}\n\n\treturn trimmed\n}\n\n// handleCloudflareQuery delegates a Cloudflare query to the Cloudflare agent\nfunc handleCloudflareQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Cloudflare agent...\")\n\t}\n\n\tvar client *cloudflare.Client\n\tvar err error\n\n\t// Check for backend API key first\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetCloudflareCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.APIToken != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Cloudflare credentials from backend\")\n\t\t\t}\n\t\t\tclient, err = cloudflare.NewClientWithCredentials(&cloudflare.BackendCloudflareCredentials{\n\t\t\t\tAPIToken: backendCreds.APIToken,\n\t\t\t\tAccountID: backendCreds.AccountID,\n\t\t\t}, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client with backend credentials: %w\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Cloudflare credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Fall back to local config if backend credentials not available\n\tif client == nil {\n\t\taccountID := cloudflare.ResolveAccountID()\n\t\tapiToken := cloudflare.ResolveAPIToken()\n\n\t\tif apiToken == \"\" {\n\t\t\treturn fmt.Errorf(\"cloudflare api_token is required (set cloudflare.api_token, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN)\")\n\t\t}\n\n\t\tclient, err = cloudflare.NewClient(accountID, apiToken, debug)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create Cloudflare client: %w\", err)\n\t\t}\n\t}\n\n\t// Determine query type\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for WAF/Security queries\n\tisWAF := strings.Contains(questionLower, \"firewall\") ||\n\t\tstrings.Contains(questionLower, \"waf\") ||\n\t\tstrings.Contains(questionLower, \"rate limit\") ||\n\t\tstrings.Contains(questionLower, \"security level\") ||\n\t\tstrings.Contains(questionLower, \"under attack\") ||\n\t\tstrings.Contains(questionLower, \"ddos\") ||\n\t\tstrings.Contains(questionLower, \"bot\")\n\n\tif isWAF {\n\t\t// Use WAF subagent\n\t\twafAgent := cfwaf.NewSubAgent(client, debug)\n\t\topts := cfwaf.QueryOptions{}\n\n\t\tresponse, err := wafAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare WAF agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfwaf.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfwaf.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfwaf.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Workers queries\n\tisWorkers := strings.Contains(questionLower, \"worker\") ||\n\t\tstrings.Contains(questionLower, \"kv\") ||\n\t\tstrings.Contains(questionLower, \"d1\") ||\n\t\tstrings.Contains(questionLower, \"r2\") ||\n\t\tstrings.Contains(questionLower, \"pages\") ||\n\t\tstrings.Contains(questionLower, \"durable object\")\n\n\tif isWorkers {\n\t\t// Use Workers subagent\n\t\tworkersAgent := cfworkers.NewSubAgent(client, debug)\n\t\topts := cfworkers.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := workersAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Workers agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfworkers.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfworkers.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfworkers.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Analytics queries\n\tisAnalytics := strings.Contains(questionLower, \"analytics\") ||\n\t\tstrings.Contains(questionLower, \"traffic\") ||\n\t\tstrings.Contains(questionLower, \"bandwidth\") ||\n\t\tstrings.Contains(questionLower, \"requests\") ||\n\t\tstrings.Contains(questionLower, \"visitors\") ||\n\t\tstrings.Contains(questionLower, \"page views\") ||\n\t\tstrings.Contains(questionLower, \"performance metrics\")\n\n\tif isAnalytics {\n\t\t// Use Analytics subagent\n\t\tanalyticsAgent := cfanalytics.NewSubAgent(client, debug)\n\t\topts := cfanalytics.QueryOptions{}\n\n\t\tresponse, err := analyticsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Analytics agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfanalytics.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfanalytics.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for Zero Trust queries\n\tisZeroTrust := strings.Contains(questionLower, \"tunnel\") ||\n\t\tstrings.Contains(questionLower, \"access app\") ||\n\t\tstrings.Contains(questionLower, \"access policy\") ||\n\t\tstrings.Contains(questionLower, \"zero trust\") ||\n\t\tstrings.Contains(questionLower, \"cloudflared\") ||\n\t\tstrings.Contains(questionLower, \"warp\")\n\n\tif isZeroTrust {\n\t\t// Use Zero Trust subagent\n\t\tztAgent := cfzerotrust.NewSubAgent(client, debug)\n\t\topts := cfzerotrust.QueryOptions{\n\t\t\tAccountID: client.GetAccountID(),\n\t\t}\n\n\t\tresponse, err := ztAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare Zero Trust agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfzerotrust.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfzerotrust.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfzerotrust.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Check for DNS queries\n\tisDNS := strings.Contains(questionLower, \"dns\") ||\n\t\tstrings.Contains(questionLower, \"record\") ||\n\t\tstrings.Contains(questionLower, \"zone\") ||\n\t\tstrings.Contains(questionLower, \"domain\") ||\n\t\tstrings.Contains(questionLower, \"cname\") ||\n\t\tstrings.Contains(questionLower, \"a record\") ||\n\t\tstrings.Contains(questionLower, \"mx\") ||\n\t\tstrings.Contains(questionLower, \"txt\") ||\n\t\tstrings.Contains(questionLower, \"nameserver\")\n\n\tif isDNS {\n\t\t// Use DNS subagent\n\t\tdnsAgent := cfdns.NewSubAgent(client, debug)\n\t\topts := cfdns.QueryOptions{}\n\n\t\tresponse, err := dnsAgent.HandleQuery(ctx, question, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Cloudflare DNS agent error: %w\", err)\n\t\t}\n\n\t\tswitch response.Type {\n\t\tcase cfdns.ResponseTypePlan:\n\t\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println(string(planJSON))\n\t\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\t\tcase cfdns.ResponseTypeResult:\n\t\t\tfmt.Println(response.Result)\n\t\tcase cfdns.ResponseTypeError:\n\t\t\treturn response.Error\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For non-DNS queries, use the general Cloudflare context\n\tcfContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Cloudflare context: %w\", err)\n\t}\n\n\t// Get AI provider settings\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\n\tvar apiKey string\n\tswitch aiProfile {\n\tcase \"gemini\", \"gemini-api\":\n\t\tapiKey = \"\"\n\tcase \"openai\":\n\t\tapiKey = viper.GetString(\"ai.providers.openai.api_key\")\n\t\tif apiKey == \"\" {\n\t\t\tapiKey = os.Getenv(\"OPENAI_API_KEY\")\n\t\t}\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(aiProfile, apiKey, debug, aiProfile)\n\n\t// Build prompt with Cloudflare context\n\tprompt := fmt.Sprintf(`You are a Cloudflare infrastructure assistant. Answer the following question based on the Cloudflare account context provided.\n\nQuestion: %s\n\nCloudflare Account Context:\n%s\n\nProvide a clear and helpful response.`, question, cfContext)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleIAMQuery delegates an IAM query to the IAM agent\nfunc handleIAMQuery(ctx context.Context, question string, debug bool, roleARN, policyARN string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to IAM agent...\")\n\t\tif roleARN != \"\" {\n\t\t\tfmt.Printf(\" Role ARN: %s\\n\", roleARN)\n\t\t}\n\t\tif policyARN != \"\" {\n\t\t\tfmt.Printf(\" Policy ARN: %s\\n\", policyARN)\n\t\t}\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\t// Create IAM agent\n\tiamAgent, err := iamclient.NewAgentWithOptions(iamclient.AgentOptions{\n\t\tProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tDebug: debug,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create IAM agent: %w\", err)\n\t}\n\n\t// Configure query options - scope to specific resource if ARN provided\n\topts := iamclient.QueryOptions{\n\t\tAccountWide: roleARN == \"\" && policyARN == \"\",\n\t\tRoleARN: roleARN,\n\t\tPolicyARN: policyARN,\n\t}\n\n\t// Handle the query\n\tresponse, err := iamAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"IAM agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase iamclient.ResponseTypePlan:\n\t\t// Output plan\n\t\tfmt.Println(response.Content)\n\t\tfmt.Println(\"\\n// To apply this plan, review the commands and run them manually\")\n\n\tcase iamclient.ResponseTypeFindings:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeResult:\n\t\tfmt.Println(response.Content)\n\n\tcase iamclient.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// handleDigitalOceanQuery delegates a Digital Ocean query to the DO client\nfunc handleDigitalOceanQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Digital Ocean agent...\")\n\t}\n\n\tapiToken := digitalocean.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"digitalocean api_token is required (set digitalocean.api_token, DO_API_TOKEN, or DIGITALOCEAN_ACCESS_TOKEN)\")\n\t}\n\n\tclient, err := digitalocean.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Digital Ocean client: %w\", err)\n\t}\n\n\tdoContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Digital Ocean context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Digital Ocean infrastructure expert. Answer the user's question based on the following Digital Ocean context data.\n\nDigital Ocean Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, doContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleHetznerQuery delegates a Hetzner Cloud query to the Hetzner client\nfunc handleHetznerQuery(ctx context.Context, question string, debug bool) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to Hetzner Cloud agent...\")\n\t}\n\n\tapiToken := hetzner.ResolveAPIToken()\n\tif apiToken == \"\" {\n\t\treturn fmt.Errorf(\"hetzner api_token is required (set hetzner.api_token or HCLOUD_TOKEN)\")\n\t}\n\n\tclient, err := hetzner.NewClient(apiToken, debug)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Hetzner client: %w\", err)\n\t}\n\n\thetznerContext, err := client.GetRelevantContext(ctx, question)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Hetzner context: %w\", err)\n\t}\n\n\t// Get AI client\n\tvar provider string\n\taiProfile := viper.GetString(\"ai.default_provider\")\n\tif aiProfile == \"\" {\n\t\taiProfile = \"openai\"\n\t}\n\tprovider = aiProfile\n\n\tvar apiKey string\n\tswitch provider {\n\tcase \"gemini\":\n\t\tapiKey = \"\"\n\tcase \"gemini-api\":\n\t\tapiKey = resolveGeminiAPIKey(\"\")\n\tcase \"openai\":\n\t\tapiKey = resolveOpenAIKey(\"\")\n\tcase \"anthropic\":\n\t\tapiKey = resolveAnthropicKey(\"\")\n\tcase \"deepseek\":\n\t\tapiKey = resolveDeepSeekKey(\"\")\n\tcase \"minimax\":\n\t\tapiKey = resolveMiniMaxKey(\"\")\n\tdefault:\n\t\tapiKey = viper.GetString(\"ai.api_key\")\n\t}\n\n\taiClient := ai.NewClient(provider, apiKey, debug, \"\")\n\n\tprompt := fmt.Sprintf(`You are a Hetzner Cloud infrastructure expert. Answer the user's question based on the following Hetzner Cloud context data.\n\nHetzner Cloud Context:\n%s\n\nUser Question: %s\n\nProvide a clear, concise answer based on the data above. If the data doesn't contain enough information to fully answer the question, say so and suggest what additional information might be needed.`, hetznerContext, question)\n\n\tresponse, err := aiClient.AskPrompt(ctx, prompt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get AI response: %w\", err)\n\t}\n\n\tfmt.Println(response)\n\treturn nil\n}\n\n// handleK8sQuery delegates a Kubernetes query to the K8s agent\nfunc handleK8sQuery(ctx context.Context, question string, debug bool, kubeconfig string) error {\n\tif debug {\n\t\tfmt.Println(\"Delegating query to K8s agent...\")\n\t}\n\n\t// Check for backend Kubernetes credentials first\n\tvar backendKubeconfigPath string\n\tbackendAPIKey := backend.ResolveAPIKey(\"\")\n\tif backendAPIKey != \"\" && kubeconfig == \"\" {\n\t\tbackendClient := backend.NewClient(backendAPIKey, debug)\n\t\tbackendCreds, backendErr := backendClient.GetKubernetesCredentials(ctx)\n\t\tif backendErr == nil && backendCreds.KubeconfigContent != \"\" {\n\t\t\tif debug {\n\t\t\t\tfmt.Println(\"[backend] Using Kubernetes credentials from backend\")\n\t\t\t}\n\t\t\t_, tempPath, err := k8s.NewClientWithCredentials(&k8s.BackendKubernetesCredentials{\n\t\t\t\tKubeconfigContent: backendCreds.KubeconfigContent,\n\t\t\t\tContextName: backendCreds.ContextName,\n\t\t\t}, debug)\n\t\t\tif err == nil {\n\t\t\t\tkubeconfig = tempPath\n\t\t\t\tbackendKubeconfigPath = tempPath\n\t\t\t\tdefer k8s.CleanupKubeconfig(backendKubeconfigPath)\n\t\t\t} else if debug {\n\t\t\t\tfmt.Printf(\"[backend] Failed to use backend kubeconfig: %v\\n\", err)\n\t\t\t}\n\t\t} else if debug {\n\t\t\tfmt.Printf(\"[backend] No Kubernetes credentials available (%v), falling back to local\\n\", backendErr)\n\t\t}\n\t}\n\n\t// Create K8s agent with AWS profile and region for EKS support\n\t// Resolve profile using same pattern as AWS client\n\tawsProfile := \"\"\n\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\tif defaultEnv == \"\" {\n\t\tdefaultEnv = \"dev\"\n\t}\n\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\tif awsProfile == \"\" {\n\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t}\n\tif awsProfile == \"\" {\n\t\tawsProfile = \"default\"\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", defaultEnv))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\n\t// Check if this is a cluster provisioning request\n\tisClusterProvisioning := (strings.Contains(questionLower, \"create\") || strings.Contains(questionLower, \"provision\") || strings.Contains(questionLower, \"setup\")) &&\n\t\t(strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"eks\") || strings.Contains(questionLower, \"kubeadm\"))\n\n\tif isClusterProvisioning {\n\t\treturn handleK8sClusterProvisioning(ctx, question, questionLower, awsProfile, awsRegion, debug)\n\t}\n\n\t// Check if this is a deployment request (creating a deployment, not listing)\n\t// Exclude read-only queries that mention \"deployment\" or \"deployments\"\n\tisReadOnlyQuery := strings.Contains(questionLower, \"list\") ||\n\t\tstrings.Contains(questionLower, \"get\") ||\n\t\tstrings.Contains(questionLower, \"show\") ||\n\t\tstrings.Contains(questionLower, \"describe\") ||\n\t\tstrings.Contains(questionLower, \"what\") ||\n\t\tstrings.Contains(questionLower, \"how\") ||\n\t\tstrings.Contains(questionLower, \"scale\") ||\n\t\tstrings.Contains(questionLower, \"rollout\") ||\n\t\tstrings.Contains(questionLower, \"status\")\n\n\t// Check for actual deploy action words (not just substring match on \"deployment\")\n\thasDeployAction := strings.Contains(questionLower, \"deploy \") ||\n\t\tstrings.HasPrefix(questionLower, \"deploy\") ||\n\t\tstrings.Contains(questionLower, \"run \")\n\n\tisDeployRequest := hasDeployAction &&\n\t\t!strings.Contains(questionLower, \"cluster\") &&\n\t\t!isReadOnlyQuery\n\n\tif isDeployRequest {\n\t\treturn handleK8sDeployment(ctx, question, questionLower, debug)\n\t}\n\n\tk8sAgent := k8s.NewAgentWithOptions(k8s.AgentOptions{\n\t\tDebug: debug,\n\t\tAWSProfile: awsProfile,\n\t\tRegion: awsRegion,\n\t\tKubeconfig: kubeconfig,\n\t})\n\n\t// Configure query options\n\topts := k8s.QueryOptions{\n\t\tClusterName: viper.GetString(\"kubernetes.default_cluster\"),\n\t\tClusterType: k8s.ClusterType(viper.GetString(\"kubernetes.default_type\")),\n\t\tNamespace: viper.GetString(\"kubernetes.default_namespace\"),\n\t\tKubeconfig: kubeconfig,\n\t}\n\n\tif opts.Namespace == \"\" {\n\t\topts.Namespace = \"default\"\n\t}\n\tif opts.ClusterType == \"\" {\n\t\topts.ClusterType = k8s.ClusterTypeExisting\n\t}\n\n\t// Handle the query\n\tresponse, err := k8sAgent.HandleQuery(ctx, question, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"K8s agent error: %w\", err)\n\t}\n\n\t// Output based on response type\n\tswitch response.Type {\n\tcase k8s.ResponseTypePlan:\n\t\t// Output plan as JSON (like AWS maker)\n\t\tplanJSON, err := json.MarshalIndent(response.Plan, \"\", \" \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t\t}\n\t\tfmt.Println(string(planJSON))\n\t\tfmt.Println(\"\\n// To apply this plan, run:\")\n\t\tfmt.Println(\"// clanker ask --apply --plan-file \")\n\n\tcase k8s.ResponseTypeResult:\n\t\tfmt.Println(response.Result)\n\n\tcase k8s.ResponseTypeError:\n\t\treturn response.Error\n\t}\n\n\treturn nil\n}\n\n// buildHelmArgs builds helm command arguments from a HelmCmd\nfunc buildHelmArgs(helmCmd k8s.HelmCmd) []string {\n\t// If raw Args are available, use them directly\n\tif len(helmCmd.Args) > 0 {\n\t\treturn helmCmd.Args\n\t}\n\n\t// Otherwise, build args from structured fields\n\tvar args []string\n\n\tswitch helmCmd.Action {\n\tcase \"install\":\n\t\targs = []string{\"install\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\t\tif helmCmd.Timeout != \"\" {\n\t\t\targs = append(args, \"--timeout\", helmCmd.Timeout)\n\t\t}\n\tcase \"upgrade\":\n\t\targs = []string{\"upgrade\", helmCmd.Release, helmCmd.Chart}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t\tif helmCmd.Wait {\n\t\t\targs = append(args, \"--wait\")\n\t\t}\n\tcase \"uninstall\":\n\t\targs = []string{\"uninstall\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\tcase \"rollback\":\n\t\targs = []string{\"rollback\", helmCmd.Release}\n\t\tif helmCmd.Namespace != \"\" {\n\t\t\targs = append(args, \"-n\", helmCmd.Namespace)\n\t\t}\n\t}\n\n\treturn args\n}\n\n// handleK8sClusterProvisioning handles cluster creation requests with plan display and approval\nfunc handleK8sClusterProvisioning(ctx context.Context, question, questionLower, awsProfile, awsRegion string, debug bool) error {\n\t// Determine cluster type from question\n\tisEKS := strings.Contains(questionLower, \"eks\")\n\tisKubeadm := strings.Contains(questionLower, \"kubeadm\") || strings.Contains(questionLower, \"ec2\")\n\n\t// Default to EKS if not specified\n\tif !isEKS && !isKubeadm {\n\t\tisEKS = true\n\t}\n\n\t// Extract cluster name from question\n\tclusterName := extractClusterName(questionLower)\n\tif clusterName == \"\" {\n\t\tclusterName = \"clanker-cluster\"\n\t}\n\n\t// Extract node count\n\tnodeCount := extractNodeCount(questionLower)\n\tif nodeCount <= 0 {\n\t\tnodeCount = 1\n\t}\n\n\t// Extract instance type\n\tinstanceType := extractInstanceType(questionLower)\n\tif instanceType == \"\" {\n\t\tinstanceType = \"t3.small\"\n\t}\n\n\tif isEKS {\n\t\treturn handleEKSCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n\t}\n\n\treturn handleKubeadmCreation(ctx, clusterName, nodeCount, instanceType, awsProfile, awsRegion, debug)\n}\n\n// handleEKSCreation handles EKS cluster creation - outputs plan JSON like AWS maker\nfunc handleEKSCreation(ctx context.Context, clusterName string, nodeCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Generate the plan\n\tk8sPlan := plan.GenerateEKSCreatePlan(plan.EKSCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tNodeCount: nodeCount,\n\t\tNodeType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create an eks cluster called %s with %d node using %s\", clusterName, nodeCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleKubeadmCreation handles kubeadm cluster creation - outputs plan JSON like AWS maker\nfunc handleKubeadmCreation(ctx context.Context, clusterName string, workerCount int, instanceType, awsProfile, awsRegion string, debug bool) error {\n\t// Default key pair name\n\tkeyPairName := fmt.Sprintf(\"clanker-%s-key\", clusterName)\n\n\t// Check/ensure SSH key exists (output to stderr so it doesn't mix with JSON)\n\tsshKeyInfo, err := plan.EnsureSSHKey(ctx, keyPairName, awsRegion, awsProfile, os.Stderr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure SSH key: %w\", err)\n\t}\n\n\tsshKeyPath := sshKeyInfo.PrivateKeyPath\n\n\t// Generate the plan\n\tk8sPlan := plan.GenerateKubeadmCreatePlan(plan.KubeadmCreateOptions{\n\t\tClusterName: clusterName,\n\t\tRegion: awsRegion,\n\t\tProfile: awsProfile,\n\t\tWorkerCount: workerCount,\n\t\tNodeType: instanceType,\n\t\tControlPlaneType: instanceType,\n\t\tKubernetesVersion: \"1.29\",\n\t\tKeyPairName: keyPairName,\n\t\tSSHKeyPath: sshKeyPath,\n\t\tCNI: \"calico\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tquestion := fmt.Sprintf(\"create a kubeadm cluster called %s with %d workers using %s\", clusterName, workerCount, instanceType)\n\tmakerPlan := k8sPlan.ToMakerPlan(question)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// handleK8sDeployment handles deployment requests - outputs plan JSON like AWS maker\nfunc handleK8sDeployment(ctx context.Context, question, questionLower string, debug bool) error {\n\t// Extract image from question\n\timage := extractImage(questionLower)\n\tif image == \"\" {\n\t\timage = \"nginx\"\n\t}\n\n\t// Extract deployment name\n\tdeployName := extractDeployName(questionLower)\n\tif deployName == \"\" {\n\t\t// Extract from image\n\t\tparts := strings.Split(image, \"/\")\n\t\tdeployName = parts[len(parts)-1]\n\t\tif idx := strings.Index(deployName, \":\"); idx > 0 {\n\t\t\tdeployName = deployName[:idx]\n\t\t}\n\t}\n\n\t// Extract port\n\tport := 80\n\n\t// Extract replicas\n\treplicas := 1\n\n\t// Extract namespace\n\tnamespace := \"default\"\n\n\t// Generate deploy plan\n\tdeployPlan := plan.GenerateDeployPlan(plan.DeployOptions{\n\t\tName: deployName,\n\t\tImage: image,\n\t\tPort: port,\n\t\tReplicas: replicas,\n\t\tNamespace: namespace,\n\t\tType: \"deployment\",\n\t})\n\n\t// Convert to maker-compatible format and output JSON (same as AWS maker)\n\tdeployQuestion := fmt.Sprintf(\"deploy %s to kubernetes\", image)\n\tmakerPlan := deployPlan.ToMakerPlan(deployQuestion)\n\tplanJSON, err := json.MarshalIndent(makerPlan, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to format plan: %w\", err)\n\t}\n\tfmt.Println(string(planJSON))\n\n\treturn nil\n}\n\n// Helper functions for parsing questions\n\nfunc extractClusterName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\t// Clean up any trailing punctuation\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractNodeCount(question string) int {\n\t// Look for \"X node\" or \"X worker\" patterns\n\twords := strings.Fields(question)\n\tfor i, word := range words {\n\t\tif (strings.Contains(word, \"node\") || strings.Contains(word, \"worker\")) && i > 0 {\n\t\t\tvar count int\n\t\t\tif _, err := fmt.Sscanf(words[i-1], \"%d\", &count); err == nil {\n\t\t\t\treturn count\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc extractInstanceType(question string) string {\n\t// Look for common instance type patterns\n\tinstanceTypes := []string{\"t3.micro\", \"t3.small\", \"t3.medium\", \"t3.large\", \"t3.xlarge\",\n\t\t\"t2.micro\", \"t2.small\", \"t2.medium\", \"t2.large\",\n\t\t\"m5.large\", \"m5.xlarge\", \"m6i.large\", \"m6i.xlarge\"}\n\tfor _, t := range instanceTypes {\n\t\tif strings.Contains(question, t) {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractImage(question string) string {\n\t// Look for common image patterns\n\twords := strings.Fields(question)\n\tfor _, word := range words {\n\t\t// Check for docker image patterns\n\t\tif strings.Contains(word, \"/\") || strings.Contains(word, \":\") {\n\t\t\treturn strings.TrimRight(word, \".,;:!?\")\n\t\t}\n\t\t// Check for common images\n\t\tcommonImages := []string{\"nginx\", \"redis\", \"postgres\", \"mysql\", \"mongo\", \"node\", \"python\", \"golang\"}\n\t\tfor _, img := range commonImages {\n\t\t\tif word == img {\n\t\t\t\treturn img\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc extractDeployName(question string) string {\n\t// Look for \"called X\" or \"named X\" patterns\n\tpatterns := []string{\"called \", \"named \", \"name \"}\n\tfor _, pattern := range patterns {\n\t\tif idx := strings.Index(question, pattern); idx != -1 {\n\t\t\trest := question[idx+len(pattern):]\n\t\t\twords := strings.Fields(rest)\n\t\t\tif len(words) > 0 {\n\t\t\t\tname := words[0]\n\t\t\t\tname = strings.TrimRight(name, \".,;:!?\")\n\t\t\t\treturn name\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatK8sCommand formats a command for display (like AWS maker formatAWSArgsForLog)\nfunc formatK8sCommand(cmdName string, args []string) string {\n\tconst maxArgLen = 160\n\tconst maxTotalLen = 700\n\n\tparts := make([]string, 0, len(args)+1)\n\tparts = append(parts, cmdName)\n\tfor _, a := range args {\n\t\tif len(a) > maxArgLen {\n\t\t\ta = a[:maxArgLen] + \"...\"\n\t\t}\n\t\tparts = append(parts, a)\n\t}\n\ts := strings.Join(parts, \" \")\n\tif len(s) > maxTotalLen {\n\t\ts = s[:maxTotalLen] + \"...\"\n\t}\n\treturn s\n}\n\n// isK8sPlan checks if a plan JSON is a K8s plan (contains eksctl, kubectl, or kubeadm commands)\nfunc isK8sPlan(rawPlan string) bool {\n\treturn strings.Contains(rawPlan, `\"eksctl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubectl\"`) ||\n\t\tstrings.Contains(rawPlan, `\"kubeadm\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm_cmds\"`) ||\n\t\tstrings.Contains(rawPlan, `\"helm\"`)\n}\n\n// executeK8sPlan executes a K8s plan (supports both K8sPlan with helm_cmds and MakerPlan formats)\nfunc executeK8sPlan(ctx context.Context, rawPlan string, profile string, debug bool) error {\n\t// First try to parse as K8sPlan (with helm_cmds)\n\tvar k8sPlan k8s.K8sPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &k8sPlan); err == nil && len(k8sPlan.HelmCmds) > 0 {\n\t\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", k8sPlan.Summary)\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t\t// Execute helm commands\n\t\ttotalSteps := len(k8sPlan.HelmCmds) + len(k8sPlan.KubectlCmds)\n\t\tstepNum := 0\n\n\t\tfor _, helmCmd := range k8sPlan.HelmCmds {\n\t\t\tstepNum++\n\t\t\targs := buildHelmArgs(helmCmd)\n\t\t\tif len(args) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: helm %s\\n\", stepNum, totalSteps, strings.Join(args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"helm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"helm command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\t// Execute kubectl commands\n\t\tfor _, kubectlCmd := range k8sPlan.KubectlCmds {\n\t\t\tstepNum++\n\t\t\tfmt.Printf(\"[k8s] running %d/%d: kubectl %s\\n\", stepNum, totalSteps, strings.Join(kubectlCmd.Args, \" \"))\n\n\t\t\tcmd := exec.CommandContext(ctx, \"kubectl\", kubectlCmd.Args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubectl command failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tfmt.Println(strings.Repeat(\"-\", 60))\n\t\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\t\treturn nil\n\t}\n\n\t// Fall back to MakerPlan format (eksctl, kubectl, kubeadm commands)\n\tvar makerPlan plan.MakerPlan\n\tif err := json.Unmarshal([]byte(rawPlan), &makerPlan); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse K8s plan: %w\", err)\n\t}\n\n\t// Resolve AWS profile\n\tawsProfile := profile\n\tif awsProfile == \"\" {\n\t\tdefaultEnv := viper.GetString(\"infra.default_environment\")\n\t\tif defaultEnv == \"\" {\n\t\t\tdefaultEnv = \"dev\"\n\t\t}\n\t\tawsProfile = viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.profile\", defaultEnv))\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = viper.GetString(\"aws.default_profile\")\n\t\t}\n\t\tif awsProfile == \"\" {\n\t\t\tawsProfile = \"default\"\n\t\t}\n\t}\n\n\t// Resolve region\n\tawsRegion := viper.GetString(fmt.Sprintf(\"infra.aws.environments.%s.region\", viper.GetString(\"infra.default_environment\")))\n\tif awsRegion == \"\" {\n\t\tawsRegion = viper.GetString(\"aws.default_region\")\n\t}\n\tif awsRegion == \"\" {\n\t\tawsRegion = \"us-east-1\"\n\t}\n\n\tfmt.Printf(\"\\n[k8s] Executing plan: %s\\n\", makerPlan.Summary)\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\n\t// Execute each command\n\tfor i, cmd := range makerPlan.Commands {\n\t\tif len(cmd.Args) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmdName := cmd.Args[0]\n\t\tcmdArgs := cmd.Args[1:]\n\n\t\t// Handle eks commands - they need to run as \"aws eks ...\"\n\t\tif cmdName == \"eks\" {\n\t\t\tcmdName = \"aws\"\n\t\t\tcmdArgs = append([]string{\"eks\"}, cmdArgs...)\n\t\t}\n\n\t\t// Add profile/region for AWS and eksctl commands\n\t\tif cmdName == \"aws\" || cmdName == \"eksctl\" {\n\t\t\tcmdArgs = append(cmdArgs, \"--profile\", awsProfile)\n\t\t\tif cmdName == \"eksctl\" {\n\t\t\t\tcmdArgs = append(cmdArgs, \"--region\", awsRegion)\n\t\t\t}\n\t\t}\n\n\t\t// Format command for display (like AWS maker)\n\t\tdisplayCmd := formatK8sCommand(cmdName, cmdArgs)\n\t\tfmt.Printf(\"[k8s] running %d/%d: %s\\n\", i+1, len(makerPlan.Commands), displayCmd)\n\n\t\t// Execute the command\n\t\texecCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)\n\t\texecCmd.Stdout = os.Stdout\n\t\texecCmd.Stderr = os.Stderr\n\n\t\tif err := execCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %s: %w\", cmdName, err)\n\t\t}\n\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(strings.Repeat(\"-\", 60))\n\tfmt.Println(\"[k8s] Plan executed successfully!\")\n\treturn nil\n}\n\n// determineRoutingDecision analyzes a question and returns which agent should handle it.\n// This is used by the --route-only flag to return routing decisions without executing.\nfunc determineRoutingDecision(question string) (agent string, reason string) {\n\tquestionLower := strings.ToLower(question)\n\n\t// Check for IAM-specific queries first\n\tiamKeywords := []string{\n\t\t\"iam role\", \"iam roles\", \"iam policy\", \"iam policies\",\n\t\t\"iam user\", \"iam users\", \"iam permission\", \"iam permissions\",\n\t\t\"trust policy\", \"assume role\", \"attached policies\",\n\t\t\"inline policies\", \"permission boundary\",\n\t\t\"access key\", \"access keys\", \"credential report\",\n\t\t\"least privilege\", \"security audit\", \"iam analysis\",\n\t\t\"overpermissive\", \"admin access\", \"cross-account trust\",\n\t\t\"mfa status\", \"unused role\", \"wildcard permission\",\n\t\t\"analyze iam\", \"fix iam\", \"iam security\",\n\t}\n\tfor _, kw := range iamKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"iam\", \"IAM query or security analysis request\"\n\t\t}\n\t}\n\n\tterraformSignals := []string{\n\t\t\"terraform\", \"tf \", \"tfstate\", \"tf plan\", \"tf apply\", \"tf destroy\",\n\t\t\"hcl\", \"module\", \"provider\", \"workspace\", \"state\", \"plan\", \"apply\", \"destroy\",\n\t\t\"drift\", \"refresh\", \"init\",\n\t}\n\tfor _, kw := range terraformSignals {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"terraform\", \"Terraform query or analysis request\"\n\t\t}\n\t}\n\n\t// Check for diagram/visualization requests\n\tdiagramKeywords := []string{\n\t\t\"diagram\", \"visual\", \"visualize\", \"layout\", \"arrange\",\n\t\t\"draw\", \"illustrate\", \"show on diagram\", \"add to diagram\",\n\t\t\"update diagram\", \"modify diagram\",\n\t}\n\tfor _, kw := range diagramKeywords {\n\t\tif strings.Contains(questionLower, kw) {\n\t\t\treturn \"diagram\", \"Diagram or visualization request detected\"\n\t\t}\n\t}\n\n\t// Action keywords for infrastructure provisioning\n\tactionKeywords := []string{\n\t\t\"create\", \"provision\", \"deploy\", \"launch\", \"spin up\", \"set up\", \"setup\",\n\t\t\"add\", \"make\", \"build\", \"install\", \"configure\", \"enable\", \"start\",\n\t\t\"update\", \"modify\", \"change\", \"scale\", \"resize\", \"upgrade\",\n\t\t\"delete\", \"remove\", \"destroy\", \"terminate\", \"tear down\", \"teardown\",\n\t}\n\n\t// K8s resources (checked first as more specific)\n\tk8sResources := []string{\n\t\t\"kubernetes\", \"k8s\", \"pod\", \"pods\", \"deployment\", \"deployments\",\n\t\t\"service\", \"services\", \"ingress\", \"namespace\", \"configmap\",\n\t\t\"secret\", \"pvc\", \"persistent volume\", \"statefulset\", \"daemonset\",\n\t\t\"replicaset\", \"cronjob\", \"job\", \"container\", \"helm\", \"chart\",\n\t\t\"kubectl\", \"eksctl\", \"kubeadm\", \"nginx\", \"redis\", \"mysql\", \"postgres\", \"mongodb\",\n\t\t\"cluster\", \"node\", \"nodes\", \"kube\",\n\t}\n\n\t// AWS resources (excluding EKS which is handled by K8s maker)\n\tawsResources := []string{\n\t\t\"ec2\", \"instance\", \"lambda\", \"function\", \"s3\", \"bucket\",\n\t\t\"rds\", \"database\", \"dynamodb\", \"table\", \"sqs\", \"queue\",\n\t\t\"sns\", \"topic\", \"ecs\", \"fargate\", \"elasticache\", \"memcached\",\n\t\t\"elb\", \"alb\", \"nlb\", \"load balancer\", \"api gateway\", \"cloudfront\", \"cdn\",\n\t\t\"route53\", \"dns\", \"iam\", \"role\", \"policy\", \"user\",\n\t\t\"vpc\", \"subnet\", \"security group\", \"nat\", \"igw\",\n\t\t\"kinesis\", \"stream\", \"glue\", \"athena\", \"redshift\",\n\t\t\"elastic beanstalk\", \"codepipeline\", \"codebuild\",\n\t}\n\n\thasAction := false\n\tfor _, action := range actionKeywords {\n\t\tif strings.Contains(questionLower, action) {\n\t\t\thasAction = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check if question mentions K8s resources\n\thasK8sResource := false\n\tfor _, resource := range k8sResources {\n\t\tif strings.Contains(questionLower, resource) {\n\t\t\thasK8sResource = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hasAction {\n\t\t// Check K8s resources first (more specific)\n\t\tif hasK8sResource {\n\t\t\treturn \"k8s-maker\", \"K8s infrastructure provisioning or modification request\"\n\t\t}\n\t\t// Check AWS resources\n\t\tfor _, resource := range awsResources {\n\t\t\tif strings.Contains(questionLower, resource) {\n\t\t\t\treturn \"maker\", \"AWS infrastructure provisioning or modification request\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// K8s read queries (no action keyword but mentions K8s resources)\n\tif hasK8sResource {\n\t\treturn \"k8s\", \"K8s query or analysis request\"\n\t}\n\n\t// Default to CLI for general queries\n\treturn \"cli\", \"General infrastructure query or analysis\"\n}\n"}}, "internal/aws/llm.go": {"4d7af4d3f13f7651fb538328b6d57eacdfc4f8d7": {"t": 1785745620.1691508, "n": 130853, "h": "80e022bc3125e7aa", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n\tLocalModelInferenceURL string `mapstructure:\"local_model_inference_url\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_scheduler_service\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Scheduler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Scheduler service is available\", nil\n\n\tcase \"check_pipes_service\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Pipes service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Pipes service is available\", nil\n\n\tcase \"check_verifiedpermissions_service\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Verified Permissions service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Verified Permissions service is available\", nil\n\n\tcase \"check_securitylake_service\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Lake service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Lake service is available\", nil\n\n\tcase \"check_datazone_service\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon DataZone service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon DataZone service is available\", nil\n\n\tcase \"check_qbusiness_service\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon Q Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon Q Business service is available\", nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"list_apprunner_services\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_batch_jobs\":\n\t\tqueueArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\", \"--query\", \"jobQueues[*].{Name:jobQueueName,State:state,Status:status,Priority:priority}\"}\n\t\tqueues, _ := c.execAWSCLI(ctx, queueArgs, profile)\n\n\t\tenvArgs := []string{\"batch\", \"describe-compute-environments\", \"--output\", \"table\", \"--query\", \"computeEnvironments[*].{Name:computeEnvironmentName,State:state,Status:status,Type:type}\"}\n\t\tenvironments, _ := c.execAWSCLI(ctx, envArgs, profile)\n\n\t\treturn fmt.Sprintf(\"AWS Batch Job Queues:\\n%s\\n\\nAWS Batch Compute Environments:\\n%s\", queues, environments), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName, _ := input[\"cluster_name\"].(string) // optional; absent → nil interface\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName, _ := input[\"cluster_name\"].(string) // optional; absent → nil interface\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_lambda_layers\":\n\t\targs := []string{\"lambda\", \"list-layers\", \"--output\", \"table\", \"--query\", \"Layers[*].{Name:LayerName,Arn:LayerArn,LatestVersion:LatestMatchingVersion.Version,Created:LatestMatchingVersion.CreatedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\", \"list_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\", \"list_cloudwatch_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\", \"list_route53_hosted_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\", \"list_secrets_manager_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_schedules\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_pipes\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\tcase \"list_verifiedpermissions_policy_stores\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_securitylake_data_lakes\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\", \"list_codepipeline_pipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_cloudformation_stacks\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--stack-status-filter\", \"CREATE_COMPLETE\", \"UPDATE_COMPLETE\", \"UPDATE_ROLLBACK_COMPLETE\", \"IMPORT_COMPLETE\", \"IMPORT_ROLLBACK_COMPLETE\", \"--output\", \"table\", \"--query\", \"StackSummaries[*].{Name:StackName,Status:StackStatus,Updated:LastUpdatedTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_training_jobs\":\n\t\targs := []string{\"sagemaker\", \"list-training-jobs\", \"--output\", \"table\", \"--query\", \"TrainingJobSummaries[*].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\tcase \"search_resource_explorer\":\n\t\targs := []string{\"resource-explorer-2\", \"search\", \"--query-string\", \"*\", \"--max-results\", \"50\", \"--output\", \"table\", \"--query\", \"Resources[*].{Arn:Arn,Service:OwningService,Region:Region,Type:ResourceType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_tagged_resources\":\n\t\targs := []string{\"resourcegroupstaggingapi\", \"get-resources\", \"--resources-per-page\", \"50\", \"--output\", \"table\", \"--query\", \"ResourceTagMappingList[*].{ARN:ResourceARN,Tags:Tags}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_qbusiness_applications\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_datazone_domains\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_scheduler_service\",\n\t\t\"check_pipes_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_securitylake_service\",\n\t\t\"check_verifiedpermissions_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_datazone_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_qbusiness_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "c58f910b1d6e14e627bb119b4e937740b6fbabb5": {"t": 1785748529.278975, "n": 130853, "h": "80e022bc3125e7aa", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n\tLocalModelInferenceURL string `mapstructure:\"local_model_inference_url\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_scheduler_service\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Scheduler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Scheduler service is available\", nil\n\n\tcase \"check_pipes_service\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Pipes service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Pipes service is available\", nil\n\n\tcase \"check_verifiedpermissions_service\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Verified Permissions service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Verified Permissions service is available\", nil\n\n\tcase \"check_securitylake_service\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Lake service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Lake service is available\", nil\n\n\tcase \"check_datazone_service\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon DataZone service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon DataZone service is available\", nil\n\n\tcase \"check_qbusiness_service\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon Q Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon Q Business service is available\", nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"list_apprunner_services\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_batch_jobs\":\n\t\tqueueArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\", \"--query\", \"jobQueues[*].{Name:jobQueueName,State:state,Status:status,Priority:priority}\"}\n\t\tqueues, _ := c.execAWSCLI(ctx, queueArgs, profile)\n\n\t\tenvArgs := []string{\"batch\", \"describe-compute-environments\", \"--output\", \"table\", \"--query\", \"computeEnvironments[*].{Name:computeEnvironmentName,State:state,Status:status,Type:type}\"}\n\t\tenvironments, _ := c.execAWSCLI(ctx, envArgs, profile)\n\n\t\treturn fmt.Sprintf(\"AWS Batch Job Queues:\\n%s\\n\\nAWS Batch Compute Environments:\\n%s\", queues, environments), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName, _ := input[\"cluster_name\"].(string) // optional; absent → nil interface\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName, _ := input[\"cluster_name\"].(string) // optional; absent → nil interface\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_lambda_layers\":\n\t\targs := []string{\"lambda\", \"list-layers\", \"--output\", \"table\", \"--query\", \"Layers[*].{Name:LayerName,Arn:LayerArn,LatestVersion:LatestMatchingVersion.Version,Created:LatestMatchingVersion.CreatedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\", \"list_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\", \"list_cloudwatch_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\", \"list_route53_hosted_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\", \"list_secrets_manager_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_schedules\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_pipes\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\tcase \"list_verifiedpermissions_policy_stores\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_securitylake_data_lakes\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\", \"list_codepipeline_pipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_cloudformation_stacks\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--stack-status-filter\", \"CREATE_COMPLETE\", \"UPDATE_COMPLETE\", \"UPDATE_ROLLBACK_COMPLETE\", \"IMPORT_COMPLETE\", \"IMPORT_ROLLBACK_COMPLETE\", \"--output\", \"table\", \"--query\", \"StackSummaries[*].{Name:StackName,Status:StackStatus,Updated:LastUpdatedTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_training_jobs\":\n\t\targs := []string{\"sagemaker\", \"list-training-jobs\", \"--output\", \"table\", \"--query\", \"TrainingJobSummaries[*].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\tcase \"search_resource_explorer\":\n\t\targs := []string{\"resource-explorer-2\", \"search\", \"--query-string\", \"*\", \"--max-results\", \"50\", \"--output\", \"table\", \"--query\", \"Resources[*].{Arn:Arn,Service:OwningService,Region:Region,Type:ResourceType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_tagged_resources\":\n\t\targs := []string{\"resourcegroupstaggingapi\", \"get-resources\", \"--resources-per-page\", \"50\", \"--output\", \"table\", \"--query\", \"ResourceTagMappingList[*].{ARN:ResourceARN,Tags:Tags}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_qbusiness_applications\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_datazone_domains\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_scheduler_service\",\n\t\t\"check_pipes_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_securitylake_service\",\n\t\t\"check_verifiedpermissions_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_datazone_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_qbusiness_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "59035c8ba313f9a7b5db3e598d8ead84f9679913": {"t": 1785748530.8623126, "n": 130787, "h": "5880cf19e6d289d8", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n\tLocalModelInferenceURL string `mapstructure:\"local_model_inference_url\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_scheduler_service\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Scheduler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Scheduler service is available\", nil\n\n\tcase \"check_pipes_service\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Pipes service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Pipes service is available\", nil\n\n\tcase \"check_verifiedpermissions_service\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Verified Permissions service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Verified Permissions service is available\", nil\n\n\tcase \"check_securitylake_service\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Lake service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Lake service is available\", nil\n\n\tcase \"check_datazone_service\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon DataZone service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon DataZone service is available\", nil\n\n\tcase \"check_qbusiness_service\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon Q Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon Q Business service is available\", nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"list_apprunner_services\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_batch_jobs\":\n\t\tqueueArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\", \"--query\", \"jobQueues[*].{Name:jobQueueName,State:state,Status:status,Priority:priority}\"}\n\t\tqueues, _ := c.execAWSCLI(ctx, queueArgs, profile)\n\n\t\tenvArgs := []string{\"batch\", \"describe-compute-environments\", \"--output\", \"table\", \"--query\", \"computeEnvironments[*].{Name:computeEnvironmentName,State:state,Status:status,Type:type}\"}\n\t\tenvironments, _ := c.execAWSCLI(ctx, envArgs, profile)\n\n\t\treturn fmt.Sprintf(\"AWS Batch Job Queues:\\n%s\\n\\nAWS Batch Compute Environments:\\n%s\", queues, environments), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_lambda_layers\":\n\t\targs := []string{\"lambda\", \"list-layers\", \"--output\", \"table\", \"--query\", \"Layers[*].{Name:LayerName,Arn:LayerArn,LatestVersion:LatestMatchingVersion.Version,Created:LatestMatchingVersion.CreatedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\", \"list_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\", \"list_cloudwatch_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\", \"list_route53_hosted_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\", \"list_secrets_manager_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_schedules\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_pipes\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\tcase \"list_verifiedpermissions_policy_stores\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_securitylake_data_lakes\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\", \"list_codepipeline_pipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_cloudformation_stacks\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--stack-status-filter\", \"CREATE_COMPLETE\", \"UPDATE_COMPLETE\", \"UPDATE_ROLLBACK_COMPLETE\", \"IMPORT_COMPLETE\", \"IMPORT_ROLLBACK_COMPLETE\", \"--output\", \"table\", \"--query\", \"StackSummaries[*].{Name:StackName,Status:StackStatus,Updated:LastUpdatedTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_training_jobs\":\n\t\targs := []string{\"sagemaker\", \"list-training-jobs\", \"--output\", \"table\", \"--query\", \"TrainingJobSummaries[*].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\tcase \"search_resource_explorer\":\n\t\targs := []string{\"resource-explorer-2\", \"search\", \"--query-string\", \"*\", \"--max-results\", \"50\", \"--output\", \"table\", \"--query\", \"Resources[*].{Arn:Arn,Service:OwningService,Region:Region,Type:ResourceType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_tagged_resources\":\n\t\targs := []string{\"resourcegroupstaggingapi\", \"get-resources\", \"--resources-per-page\", \"50\", \"--output\", \"table\", \"--query\", \"ResourceTagMappingList[*].{ARN:ResourceARN,Tags:Tags}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_qbusiness_applications\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_datazone_domains\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_scheduler_service\",\n\t\t\"check_pipes_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_securitylake_service\",\n\t\t\"check_verifiedpermissions_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_datazone_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_qbusiness_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "d0053d14493d2f82c6bb815a0d5baed610112adb": {"t": 1785748532.3522966, "n": 127913, "h": "7ede9ad1b3887f96", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n\tLocalModelInferenceURL string `mapstructure:\"local_model_inference_url\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_scheduler_service\":\n\t\targs := []string{\"scheduler\", \"list-schedules\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Scheduler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Scheduler service is available\", nil\n\n\tcase \"check_pipes_service\":\n\t\targs := []string{\"pipes\", \"list-pipes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge Pipes service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ EventBridge Pipes service is available\", nil\n\n\tcase \"check_verifiedpermissions_service\":\n\t\targs := []string{\"verifiedpermissions\", \"list-policy-stores\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Verified Permissions service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Verified Permissions service is available\", nil\n\n\tcase \"check_securitylake_service\":\n\t\targs := []string{\"securitylake\", \"list-data-lakes\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Lake service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Lake service is available\", nil\n\n\tcase \"check_datazone_service\":\n\t\targs := []string{\"datazone\", \"list-domains\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon DataZone service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon DataZone service is available\", nil\n\n\tcase \"check_qbusiness_service\":\n\t\targs := []string{\"qbusiness\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amazon Q Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Amazon Q Business service is available\", nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\", \"list_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\", \"list_cloudwatch_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\", \"list_route53_hosted_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\", \"list_secrets_manager_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\", \"list_codepipeline_pipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_training_jobs\":\n\t\targs := []string{\"sagemaker\", \"list-training-jobs\", \"--output\", \"table\", \"--query\", \"TrainingJobSummaries[*].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_scheduler_service\",\n\t\t\"check_pipes_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_securitylake_service\",\n\t\t\"check_verifiedpermissions_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_datazone_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_qbusiness_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "e41a40bad5024b1f410caeee5ca0b749a5ad3d86": {"t": 1785748533.853317, "n": 125387, "h": "fd5906dfa92d03f1", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n\tLocalModelInferenceURL string `mapstructure:\"local_model_inference_url\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "b033083f56b59465b0c5711de178f071c527e100": {"t": 1785748535.420918, "n": 125253, "h": "95fed7e0ffc98be1", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"github-models\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"github-models\",\n\t\t\t\tModel: \"openai/gpt-5.4\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "350625344ff2b7a59f71acad6a6650acc9bb40c8": {"t": 1785748536.9575193, "n": 125134, "h": "8ce0cf6c91421023", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"cohere\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"cohere\",\n\t\t\t\tModel: \"command-a-03-2025\",\n\t\t\t\tAPIKeyEnv: \"COHERE_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "f66a3aaf9dc87834ca5519d0cae4e1740bd936c1": {"t": 1785748538.4729218, "n": 124991, "h": "19a16eabff3a36a7", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tswitch providerName {\n\t\tcase \"openai\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"deepseek\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\tModel: \"deepseek-chat\",\n\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t}, nil\n\t\tcase \"minimax\":\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"minimax\",\n\t\t\t\tModel: \"MiniMax-M2.5\",\n\t\t\t\tAPIKeyEnv: \"MINIMAX_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "024970901440861b20f540bcce4360ba0f5d5e9c": {"t": 1785748540.0019248, "n": 124703, "h": "b1c154b669e14a52", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\t// Allow a minimal OpenAI setup without a config file.\n\t\t// The API key can still come from flags/env; this just supplies a default model.\n\t\tif providerName == \"openai\" {\n\t\t\treturn &AIProfile{\n\t\t\t\tProvider: \"openai\",\n\t\t\t\tModel: \"gpt-5\",\n\t\t\t\tAPIKeyEnv: \"OPENAI_API_KEY\",\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "cf4301ab6ac058596143849afa18decefbdc8119": {"t": 1785748541.4664974, "n": 124412, "h": "8f43f7d1ae6a8a50", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_groups\":\n\t\targs := []string{\"iam\", \"list-groups\", \"--output\", \"table\", \"--query\", \"Groups[*].{GroupName:GroupName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"debug\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "841116122c944a69535b895442c7d1bb3c6ace3e": {"t": 1785748543.0971923, "n": 124218, "h": "12a7fc7039996dcd", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// If specific log group parameters are provided, fetch targeted logs\n\t\tif input != nil {\n\t\t\tif lg, ok := input[\"log_group_name\"].(string); ok && lg != \"\" {\n\t\t\t\t// Defaults\n\t\t\t\thoursBack := 24\n\t\t\t\tif hb, ok := input[\"hours_back\"]; ok {\n\t\t\t\t\tswitch v := hb.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\thoursBack = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\thoursBack = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlimit := 200\n\t\t\t\tif lim, ok := input[\"limit\"]; ok {\n\t\t\t\t\tswitch v := lim.(type) {\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tlimit = v\n\t\t\t\t\tcase int32:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase int64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tlimit = int(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilterPattern := \"\"\n\t\t\t\tif fp, ok := input[\"filter_pattern\"].(string); ok {\n\t\t\t\t\tfilterPattern = fp\n\t\t\t\t}\n\n\t\t\t\tstartMillis := fmt.Sprintf(\"%d\", time.Now().Add(-time.Duration(hoursBack)*time.Hour).Unix()*1000)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\t\"--log-group-name\", lg,\n\t\t\t\t\t\"--start-time\", startMillis,\n\t\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t\t\t\"--limit\", fmt.Sprintf(\"%d\", limit),\n\t\t\t\t}\n\t\t\t\tif filterPattern != \"\" {\n\t\t\t\t\targs = append(args, \"--filter-pattern\", filterPattern)\n\t\t\t\t}\n\n\t\t\t\tif verbose {\n\t\t\t\t\tfmt.Printf(\"🔍 %s: Filtering logs for %s (last %dh, limit %d, pattern '%s')\\n\", toolName, lg, hoursBack, limit, filterPattern)\n\t\t\t\t}\n\n\t\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Sprintf(\"❌ Failed to get logs for %s: %v\", lg, err), nil\n\t\t\t\t}\n\n\t\t\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FROM %s\\n\", lg)\n\t\t\t\tanalysis += \"===============================\\n\\n\"\n\t\t\t\tanalysis += result + \"\\n\"\n\t\t\t\treturn analysis, nil\n\t\t\t}\n\t\t}\n\t\t// Otherwise, use the existing generic recent error logs\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "3ab3f0606ad7b25c1ca958995591137405e9f93d": {"t": 1785748544.6244538, "n": 122457, "h": "3f43d53aa39760f6", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\t// AppFlow may not be available in all regions\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"AppFlow\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"applicationcostprofiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\tresult, err := c.execAWSCLIWithGracefulError(ctx, args, profile, \"Application Cost Profiler\")\n\t\tif err != nil {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\t// Alexa for Business was deprecated and shut down\n\t\treturn \"❌ Alexa for Business service has been discontinued\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository service is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"5\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"search-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"describe-subscription\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no subscription\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\tcase \"analyze_ecs_service_logs\":\n\t\tserviceName, ok := input[\"service_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"service_name parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string) // optional\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// ECS services typically log to /ecs/service-name or /aws/ecs/service-name\n\t\tlogGroups := []string{\n\t\t\tfmt.Sprintf(\"/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/%s\", serviceName),\n\t\t\tfmt.Sprintf(\"/aws/ecs/containerinsights/%s/performance\", clusterName),\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ECS SERVICE LOG ANALYSIS FOR %s\\n\", serviceName)\n\t\tanalysis += \"=========================================\\n\\n\"\n\n\t\tfor _, logGroup := range logGroups {\n\t\t\targs := []string{\n\t\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\t\"--log-group-name\", logGroup,\n\t\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\t\"--output\", \"json\",\n\t\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"🔍 %s: Checking log group %s for service %s\\n\", toolName, logGroup, serviceName)\n\t\t\t}\n\n\t\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\t\tif err == nil && result != \"[]\" {\n\t\t\t\tanalysis += fmt.Sprintf(\"🚨 ERRORS in %s:\\n\", logGroup)\n\t\t\t\tanalysis += result + \"\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn analysis, nil\n\n\tcase \"get_ecs_task_logs\":\n\t\ttaskArn, ok := input[\"task_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"task_arn parameter required\")\n\t\t}\n\t\tclusterName := input[\"cluster_name\"].(string)\n\t\tif clusterName == \"\" {\n\t\t\tclusterName = \"default\"\n\t\t}\n\n\t\t// Get task definition to find log configuration\n\t\ttaskArgs := []string{\n\t\t\t\"ecs\", \"describe-tasks\",\n\t\t\t\"--cluster\", clusterName,\n\t\t\t\"--tasks\", taskArn,\n\t\t\t\"--output\", \"json\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting task details for %s\\n\", toolName, taskArn)\n\t\t}\n\n\t\ttaskResult, err := c.execAWSCLI(ctx, taskArgs, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get task details: %v\", err), nil\n\t\t}\n\n\t\tanalysis := \"📋 ECS TASK LOG ANALYSIS\\n\"\n\t\tanalysis += \"===========================\\n\\n\"\n\t\tanalysis += \"🔧 Task Details:\\n\"\n\t\tanalysis += taskResult + \"\\n\\n\"\n\n\t\treturn analysis, nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"analyze_lambda_errors\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent error logs from the last 24 hours\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"ERROR\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing errors for %s in log group %s\\n\", toolName, functionName, logGroupName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get error logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\t// Also get function configuration for context\n\t\tconfigArgs := []string{\"lambda\", \"get-function-configuration\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\tconfigResult, configErr := c.execAWSCLI(ctx, configArgs, profile)\n\n\t\tanalysis := fmt.Sprintf(\"🔍 ERROR ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"================================\\n\\n\"\n\n\t\tif configErr == nil {\n\t\t\tanalysis += \"📋 FUNCTION CONFIGURATION:\\n\"\n\t\t\tanalysis += configResult + \"\\n\\n\"\n\t\t}\n\n\t\tanalysis += \"🚨 ERROR LOGS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"analyze_lambda_performance\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get recent logs with duration and memory usage patterns\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-24*time.Hour).Unix()*1000),\n\t\t\t\"--filter-pattern\", \"[REPORT]\",\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Analyzing performance for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get performance logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📊 PERFORMANCE ANALYSIS FOR %s\\n\", functionName)\n\t\tanalysis += \"=====================================\\n\\n\"\n\t\tanalysis += \"⏱️ EXECUTION REPORTS (Last 24 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\tcase \"get_lambda_recent_logs\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\tlogGroupName := fmt.Sprintf(\"/aws/lambda/%s\", functionName)\n\n\t\t// Get all recent logs (last 6 hours)\n\t\targs := []string{\n\t\t\t\"logs\", \"filter-log-events\",\n\t\t\t\"--log-group-name\", logGroupName,\n\t\t\t\"--start-time\", fmt.Sprintf(\"%d\", time.Now().Add(-6*time.Hour).Unix()*1000),\n\t\t\t\"--output\", \"json\",\n\t\t\t\"--query\", \"events[*].{Timestamp:timestamp,Message:message}\",\n\t\t}\n\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting recent logs for %s\\n\", toolName, functionName)\n\t\t}\n\n\t\tresult, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn fmt.Sprintf(\"❌ Failed to get recent logs for %s: %v\", functionName, err), nil\n\t\t}\n\n\t\tanalysis := fmt.Sprintf(\"📝 RECENT LOGS FOR %s\\n\", functionName)\n\t\tanalysis += \"============================\\n\\n\"\n\t\tanalysis += \"🕐 LOGS (Last 6 hours):\\n\"\n\t\tanalysis += result + \"\\n\"\n\n\t\treturn analysis, nil\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// Use the existing log functionality\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n\n// categorizeAWSError categorizes AWS CLI errors to provide better user feedback\nfunc categorizeAWSError(err error, serviceName string) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\tswitch {\n\tcase strings.Contains(errStr, \"could not connect to the endpoint\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not available in this region\", serviceName)\n\tcase strings.Contains(errStr, \"invalid choice\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not supported by current AWS CLI version\", serviceName)\n\tcase strings.Contains(errStr, \"account not initialized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service requires account initialization\", serviceName)\n\tcase strings.Contains(errStr, \"accessdenied\"):\n\t\treturn fmt.Sprintf(\"❌ %s service access denied - insufficient permissions\", serviceName)\n\tcase strings.Contains(errStr, \"parameter validation failed\"):\n\t\treturn fmt.Sprintf(\"❌ %s service parameter validation error\", serviceName)\n\tcase strings.Contains(errStr, \"service has been discontinued\"):\n\t\treturn fmt.Sprintf(\"❌ %s service has been discontinued\", serviceName)\n\tcase strings.Contains(errStr, \"not authorized\"):\n\t\treturn fmt.Sprintf(\"❌ %s service not authorized for this account\", serviceName)\n\tdefault:\n\t\treturn fmt.Sprintf(\"❌ %s service not available or no access\", serviceName)\n\t}\n}\n\n// execAWSCLIWithGracefulError executes AWS CLI with better error categorization\nfunc (c *Client) execAWSCLIWithGracefulError(ctx context.Context, args []string, profile *AIProfile, serviceName string) (string, error) {\n\toutput, err := c.execAWSCLI(ctx, args, profile)\n\tif err != nil {\n\t\treturn categorizeAWSError(err, serviceName), nil\n\t}\n\treturn output, nil\n}\n"}, "a87f7847d3ae6fbe26220787850f785b643acbc0": {"t": 1785748546.5932443, "n": 123032, "h": "a7906921b200ed0f", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\tif verbose {\n\t\tfmt.Printf(\"🔍 %s: Starting AWS operation with profile: %s, region: %s\\n\", toolName, profile.AWSProfile, profile.Region)\n\t}\n\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"✅ %s: Service is available, getting count...\\n\", toolName)\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Getting count with: aws %s\\n\", toolName, strings.Join(queueCountArgs, \" \"))\n\t\t}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\tif verbose {\n\t\t\tfmt.Printf(\"📊 %s: Raw count result: '%s'\\n\", toolName, countResult)\n\t\t}\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\tif verbose {\n\t\t\tfmt.Printf(\"🔍 %s: Checking service availability with: aws %s\\n\", toolName, strings.Join(args, \" \"))\n\t\t}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif verbose {\n\t\t\t\tfmt.Printf(\"❌ %s: Service check failed: %v\\n\", toolName, err)\n\t\t\t}\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppFlow service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"application-cost-profiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Cost Profiler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\targs := []string{\"alexaforbusiness\", \"list-skills\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Alexa for Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Alexa for Business service is available\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"list-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"list-subscriptions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// Use the existing log functionality\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\tverbose := viper.GetBool(\"verbose\")\n\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug || verbose {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\n\tstart := time.Now()\n\toutput, err := cmd.CombinedOutput()\n\tduration := time.Since(start)\n\n\tif err != nil {\n\t\tif c.debug || verbose {\n\t\t\tfmt.Printf(\"❌ Command failed (%v): %v\\nOutput: %s\\nCommand: %s\\n\",\n\t\t\t\tduration, err, string(output), strings.Join(cmd.Args, \" \"))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug || verbose {\n\t\toutputLen := len(output)\n\t\tif outputLen > 200 {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %d bytes output (truncated): %s...\\n\",\n\t\t\t\tduration, outputLen, string(output[:200]))\n\t\t} else {\n\t\t\tfmt.Printf(\"✅ Command succeeded (%v): %s\\n\", duration, string(output))\n\t\t}\n\t}\n\treturn string(output), nil\n}\n\n// GetLLMAnalysisPrompt returns the prompt for LLM to analyze what AWS operations are needed\nfunc GetLLMAnalysisPrompt(question string) string {\n\treturn fmt.Sprintf(`Analyze this user query and determine what AWS operations would be needed to answer it accurately. \n\nUser Query: \"%s\"\n\nAvailable AWS READ-ONLY operations (all are safe and never modify/delete anything):\n\nINFRASTRUCTURE DISCOVERY (New Enhanced Operations):\n- discover_all_active_services: Automatically discover all active AWS services by running service checks in parallel\n- get_infrastructure_overview: Get a comprehensive overview of the entire infrastructure across all services\n- check_all_services_parallel: Run all service availability checks in parallel to map the infrastructure\n\nTERRAFORM INTEGRATION:\n- get_terraform_outputs: Get terraform outputs from the configured workspace\n- get_terraform_state_summary: Get a summary of terraform state resources\n\nSERVICE EXISTENCE CHECKS (Quick checks to see if services exist and their basic counts):\n- check_sqs_service: Check if SQS service is available and count queues\n- check_eventbridge_service: Check if EventBridge service is available and count rules\n- check_lambda_service: Check if Lambda service is available and count functions\n- check_sns_service: Check if SNS service is available and count topics\n- check_dynamodb_service: Check if DynamoDB service is available and count tables\n- check_s3_service: Check if S3 service is available and count buckets\n- check_rds_service: Check if RDS service is available and count instances\n- check_ec2_service: Check if EC2 service is available and count instances\n- check_ecs_service: Check if ECS service is available and count clusters\n- check_ecr_service: Check if ECR service is available and count repositories\n\nCOMPUTE:\n- list_ec2_instances: List EC2 instances with state, type, and details\n- describe_instance: Get detailed info about a specific EC2 instance\n- list_ecs_clusters: List ECS clusters and their running services/tasks\n- describe_ecs_service: Get details about a specific ECS service\n- list_batch_jobs: List AWS Batch jobs and their status\n- list_auto_scaling_groups: List Auto Scaling Groups with instance counts and capacity\n- describe_auto_scaling_group: Get detailed ASG configuration and instances\n- list_launch_templates: List EC2 Launch Templates and their versions\n- describe_launch_template: Get detailed Launch Template configuration\n\nSERVERLESS:\n- list_lambda_functions: List Lambda functions with runtime and last modified\n- describe_lambda_function: Get detailed config for a specific Lambda function\n- list_lambda_layers: List Lambda layers available\n\nCONTAINER SERVICES:\n- list_ecr_repositories: List ECR repositories with URIs and creation dates\n- describe_ecr_repository: Get images and details for a specific ECR repository\n- list_eks_clusters: List EKS Kubernetes clusters with status and details\n- describe_eks_cluster: Get detailed EKS cluster configuration\n\nSTORAGE:\n- list_s3_buckets: List S3 buckets with creation dates and regions\n- describe_s3_bucket: Get details about a specific S3 bucket (size, objects, etc.)\n- list_ebs_volumes: List EBS volumes and their attachments, size, type\n- describe_ebs_volume: Get detailed info about a specific EBS volume\n- list_efs_filesystems: List EFS file systems with performance modes\n- describe_efs_filesystem: Get detailed EFS configuration and mount targets\n\nDATABASE:\n- list_rds_instances: List RDS database instances with status and config\n- describe_rds_instance: Get detailed info about a specific RDS instance\n- list_rds_clusters: List RDS Aurora clusters with engine and status\n- list_dynamodb_tables: List DynamoDB tables\n- describe_dynamodb_table: Get detailed DynamoDB table schema and settings\n\nNETWORKING:\n- list_vpcs: List VPCs and their CIDR blocks\n- list_subnets: List subnets across VPCs\n- list_security_groups: List security groups and their rules\n- describe_load_balancers: List and describe load balancers (ALB/NLB/CLB)\n- list_route_tables: List route tables and their routes\n\nMESSAGE QUEUING & EVENTS:\n- list_sqs_queues: List SQS queues with URLs and attributes\n- describe_sqs_queue: Get detailed SQS queue configuration and metrics\n- list_sns_topics: List SNS topics and their ARNs\n- describe_sns_topic: Get SNS topic configuration and subscriptions\n- list_eventbridge_rules: List EventBridge rules with schedules and targets\n- list_eventbridge_buses: List custom EventBridge event buses\n\nMONITORING & LOGS:\n- get_recent_logs: Get recent CloudWatch logs and errors\n- list_cloudwatch_alarms: List CloudWatch alarms and their status\n- describe_cloudwatch_metrics: Get CloudWatch metrics for resources\n- list_log_groups: List CloudWatch log groups\n\nSECURITY & IAM:\n- list_iam_roles: List IAM roles (names only, no sensitive data)\n- list_iam_users: List IAM users (names only, no sensitive data)\n- describe_security_groups: Get security group rules and associations\n- list_kms_keys: List KMS encryption keys\n- describe_kms_key: Get KMS key details and policies\n- list_acm_certificates: List SSL/TLS certificates with status\n- describe_acm_certificate: Get certificate details and validation\n- list_waf_webacls: List WAF Web ACLs for regional and CloudFront\n\nDEVOPS & CI/CD:\n- list_codebuild_projects: List CodeBuild projects with build history\n- list_codepipelines: List CodePipeline pipelines with status\n- describe_codepipeline: Get detailed pipeline configuration and stages\n- list_codecommit_repositories: List CodeCommit Git repositories\n\nANALYTICS & BIG DATA:\n- list_kinesis_streams: List Kinesis data streams\n- describe_kinesis_stream: Get Kinesis stream shards and throughput\n- list_glue_jobs: List AWS Glue ETL jobs and schedules\n- list_glue_databases: List Glue Data Catalog databases\n- list_emr_clusters: List EMR big data clusters\n\nMACHINE LEARNING:\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n\nCACHING:\n- list_elasticache_clusters: List ElastiCache Redis/Memcached clusters\n- describe_elasticache_cluster: Get cache cluster configuration and nodes\n\nAPPLICATION INTEGRATION:\n- list_step_functions: List Step Functions state machines\n- describe_step_function: Get Step Function workflow definition\n\nCOST & BILLING:\n- get_cost_and_usage: Get cost information and usage metrics\n- list_budgets: List AWS Budgets and spending alerts\n\nAI/ML SERVICES:\n- list_bedrock_foundation_models: List available Bedrock foundation models\n- list_bedrock_custom_models: List custom Bedrock models\n- list_bedrock_agents: List Bedrock agents\n- list_bedrock_knowledge_bases: List Bedrock knowledge bases\n- list_bedrock_guardrails: List Bedrock guardrails\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n- list_comprehend_jobs: List Comprehend analysis jobs\n- list_textract_jobs: List Textract document analysis jobs\n- list_rekognition_collections: List Rekognition face collections\n\nOTHER SERVICES:\n- list_api_gateways: List API Gateway REST and HTTP APIs\n- list_cloudfront_distributions: List CloudFront distributions\n- list_route53_zones: List Route53 hosted zones\n- list_secrets: List AWS Secrets Manager secrets (names only)\n- list_ssm_parameters: List Systems Manager parameters (names only)\n\nRespond with ONLY a JSON object in this format:\n{\n \"operations\": [\n {\n \"operation\": \"operation_name\",\n \"reason\": \"why this operation is needed\",\n \"parameters\": {\"key\": \"value\"}\n }\n ],\n \"analysis\": \"brief explanation of what the user wants to know\"\n}\n\nIf no AWS operations are needed, return: {\"operations\": [], \"analysis\": \"explanation\"}`, question)\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n"}, "c171cbbe8bc4a28e1821d9b56383d6f9cc32be4c": {"t": 1785748549.0679142, "n": 124201, "h": "faed44dc11294de2", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\ttfclient \"github.com/bgdnvk/clanker/internal/terraform\"\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// ExecuteOperationsConcurrently executes multiple AWS operations concurrently for LLM processing\nfunc (c *Client) ExecuteOperationsConcurrently(ctx context.Context, operations []LLMOperation, aiProfile string) (string, error) {\n\tif len(operations) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// Get AI profile configuration\n\tprofile, err := GetAIProfile(aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// ExecuteOperationsWithAWSProfile executes multiple AWS operations concurrently using a direct AWS profile\nfunc (c *Client) ExecuteOperationsWithAWSProfile(ctx context.Context, operations []LLMOperation, awsProfile, region string) (string, error) {\n\tif len(operations) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// Create a temporary AI profile with the specified AWS profile\n\tprofile := &AIProfile{\n\t\tProvider: \"bedrock\", // Not used for AWS operations\n\t\tAWSProfile: awsProfile,\n\t\tRegion: region,\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// executeOperationsWithProfile executes operations with a given profile\nfunc (c *Client) executeOperationsWithProfile(ctx context.Context, operations []LLMOperation, profile *AIProfile) (string, error) {\n\n\t// Create channels for results\n\tresultChan := make(chan LLMOperationResult, len(operations))\n\tvar wg sync.WaitGroup\n\n\t// Execute all operations concurrently\n\tfor i, op := range operations {\n\t\twg.Add(1)\n\t\tgo func(index int, operation string, params map[string]interface{}) {\n\t\t\tdefer wg.Done()\n\t\t\tresult, err := c.executeAWSOperation(ctx, operation, params, profile)\n\t\t\tresultChan <- LLMOperationResult{\n\t\t\t\tOperation: operation,\n\t\t\t\tResult: result,\n\t\t\t\tError: err,\n\t\t\t\tIndex: index,\n\t\t\t}\n\t\t}(i, op.Operation, op.Parameters)\n\t}\n\n\t// Wait for all operations to complete\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(resultChan)\n\t}()\n\n\t// Collect results in order\n\tresults := make([]LLMOperationResult, len(operations))\n\tfor result := range resultChan {\n\t\tresults[result.Index] = result\n\t}\n\n\t// Build results string in original order\n\tvar awsResults strings.Builder\n\tfor _, result := range results {\n\t\tif result.Error != nil {\n\t\t\tawsResults.WriteString(fmt.Sprintf(\"❌ %s failed: %v\\n\", result.Operation, result.Error))\n\t\t} else {\n\t\t\tawsResults.WriteString(fmt.Sprintf(\"✅ %s:\\n%s\\n\\n\", result.Operation, result.Result))\n\t\t}\n\t}\n\n\treturn awsResults.String(), nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// ADDITIONAL AWS SERVICES\n\tcase \"check_elasticsearch_service\":\n\t\targs := []string{\"es\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elasticsearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"es\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elasticsearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_opensearch_service\":\n\t\targs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ OpenSearch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"opensearch\", \"list-domain-names\", \"--output\", \"json\", \"--query\", \"length(DomainNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ OpenSearch service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eks_service\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EKS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"eks\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EKS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elasticache_service\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ElastiCache service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"json\", \"--query\", \"length(CacheClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ElastiCache service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_redshift_service\":\n\t\targs := []string{\"redshift\", \"describe-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Redshift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"redshift\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Redshift service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesis_service\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesis\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudformation_service\":\n\t\targs := []string{\"cloudformation\", \"list-stacks\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudFormation service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudformation\", \"list-stacks\", \"--output\", \"json\", \"--query\", \"length(StackSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFormation service is available. Stack count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kms_service\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ KMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kms\", \"list-keys\", \"--output\", \"json\", \"--query\", \"length(Keys)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ KMS service is available. Key count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_secretsmanager_service\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Secrets Manager service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"json\", \"--query\", \"length(SecretList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Secrets Manager service is available. Secret count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_route53_service\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Route53 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"json\", \"--query\", \"length(HostedZones)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Route53 service is available. Hosted zone count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudfront_service\":\n\t\t// Try standard endpoint first, fallback if FIPS endpoint fails\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\t// Check if it's a FIPS endpoint issue\n\t\t\tif strings.Contains(err.Error(), \"cloudfront-fips\") {\n\t\t\t\treturn \"❌ CloudFront service not available (FIPS endpoint not supported in this region)\", nil\n\t\t\t}\n\t\t\treturn \"❌ CloudFront service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"json\", \"--query\", \"length(DistributionList.Items || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudFront service is available. Distribution count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apigateway_service\":\n\t\targs := []string{\"apigateway\", \"get-rest-apis\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ API Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"json\", \"--query\", \"length(items)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ API Gateway service is available. REST API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_bedrock_service\":\n\t\t// Bedrock doesn't support max-results, but we can check if service is available\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"not supported\") || strings.Contains(err.Error(), \"not available\") {\n\t\t\t\treturn \"❌ Bedrock service not available in this region\", nil\n\t\t\t}\n\t\t\treturn \"❌ Bedrock service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"json\", \"--query\", \"length(modelSummaries || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Bedrock service is available. Model count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codecommit_service\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeCommit service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeCommit service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codebuild_service\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeBuild service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codebuild\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeBuild service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codepipeline_service\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodePipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodePipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sagemaker_service\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SageMaker service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"json\", \"--query\", \"length(Endpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SageMaker service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glue_service\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glue service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glue\", \"get-databases\", \"--output\", \"json\", \"--query\", \"length(DatabaseList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glue service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_athena_service\":\n\t\targs := []string{\"athena\", \"list-work-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Athena service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"athena\", \"list-work-groups\", \"--output\", \"json\", \"--query\", \"length(WorkGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Athena service is available. Work group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emr_service\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_stepfunctions_service\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Step Functions service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"json\", \"--query\", \"length(stateMachines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Step Functions service is available. State machine count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudwatch_service\":\n\t\targs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudwatch\", \"list-metrics\", \"--max-items\", \"10\", \"--output\", \"json\", \"--query\", \"length(Metrics || `[]`)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch service is available. Sample metric count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_logs_service\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudWatch Logs service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"json\", \"--query\", \"length(logGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudWatch Logs service is available. Log group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_xray_service\":\n\t\t// X-Ray requires time range, use a minimal recent range (max 6 hours)\n\t\tendTime := time.Now()\n\t\tstartTime := endTime.Add(-5 * time.Hour) // Use 5 hours to stay well under 6 hour limit\n\t\targs := []string{\"xray\", \"get-service-graph\",\n\t\t\t\"--start-time\", startTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--end-time\", endTime.Format(\"2006-01-02T15:04:05Z\"),\n\t\t\t\"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"Time range\") {\n\t\t\t\treturn \"❌ X-Ray service available but time range validation failed\", nil\n\t\t\t}\n\t\t\treturn \"❌ X-Ray service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ X-Ray service is available\", nil\n\n\tcase \"check_cognito_service\":\n\t\targs := []string{\"cognito-idp\", \"list-user-pools\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cognito service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cognito-idp\", \"list-user-pools\", \"--output\", \"json\", \"--query\", \"length(UserPools)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cognito service is available. User pool count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_wafv2_service\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WAFv2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"json\", \"--query\", \"length(WebACLs)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WAFv2 service is available. Web ACL count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_acm_service\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ACM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm\", \"list-certificates\", \"--output\", \"json\", \"--query\", \"length(CertificateSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ACM service is available. Certificate count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudtrail_service\":\n\t\targs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudTrail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloudtrail\", \"describe-trails\", \"--output\", \"json\", \"--query\", \"length(trailList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CloudTrail service is available. Trail count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_config_service\":\n\t\targs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Config service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"configservice\", \"describe-configuration-recorders\", \"--output\", \"json\", \"--query\", \"length(ConfigurationRecorders)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Config service is available. Recorder count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_guardduty_service\":\n\t\targs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GuardDuty service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"guardduty\", \"list-detectors\", \"--output\", \"json\", \"--query\", \"length(DetectorIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GuardDuty service is available. Detector count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ssm_service\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSM service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"json\", \"--query\", \"length(Parameters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SSM service is available. Parameter count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_batch_service\":\n\t\targs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Batch service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"batch\", \"describe-job-queues\", \"--output\", \"json\", \"--query\", \"length(jobQueues)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Batch service is available. Job queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appsync_service\":\n\t\targs := []string{\"appsync\", \"list-graphql-apis\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appsync\", \"list-graphql-apis\", \"--output\", \"json\", \"--query\", \"length(graphqlApis)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ AppSync service is available. GraphQL API count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_amplify_service\":\n\t\targs := []string{\"amplify\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Amplify service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"amplify\", \"list-apps\", \"--output\", \"json\", \"--query\", \"length(apps)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Amplify service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_comprehend_service\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Comprehend service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Comprehend service is available\", nil\n\n\tcase \"check_textract_service\":\n\t\targs := []string{\"textract\", \"list-adapters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Textract service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Textract service is available\", nil\n\n\tcase \"check_rekognition_service\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Rekognition service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rekognition\", \"list-collections\", \"--output\", \"json\", \"--query\", \"length(CollectionIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Rekognition service is available. Collection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_polly_service\":\n\t\targs := []string{\"polly\", \"describe-voices\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Polly service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"polly\", \"describe-voices\", \"--output\", \"json\", \"--query\", \"length(Voices)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Polly service is available. Voice count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transcribe_service\":\n\t\targs := []string{\"transcribe\", \"list-transcription-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transcribe service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Transcribe service is available\", nil\n\n\tcase \"check_translate_service\":\n\t\targs := []string{\"translate\", \"list-languages\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Translate service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"translate\", \"list-languages\", \"--output\", \"json\", \"--query\", \"length(Languages)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Translate service is available. Language count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_personalize_service\":\n\t\targs := []string{\"personalize\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Personalize service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Personalize service is available\", nil\n\n\tcase \"check_kendra_service\":\n\t\targs := []string{\"kendra\", \"list-indices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kendra service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kendra\", \"list-indices\", \"--output\", \"json\", \"--query\", \"length(IndexConfigurationSummaryItems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kendra service is available. Index count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lex_service\":\n\t\targs := []string{\"lexv2-models\", \"list-bots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lex service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lexv2-models\", \"list-bots\", \"--output\", \"json\", \"--query\", \"length(botSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lex service is available. Bot count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_apprunner_service\":\n\t\targs := []string{\"apprunner\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Runner service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"apprunner\", \"list-services\", \"--output\", \"json\", \"--query\", \"length(ServiceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Runner service is available. Service count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_documentdb_service\":\n\t\targs := []string{\"docdb\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DocumentDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"docdb\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DocumentDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_neptune_service\":\n\t\targs := []string{\"neptune\", \"describe-db-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Neptune service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"neptune\", \"describe-db-clusters\", \"--output\", \"json\", \"--query\", \"length(DBClusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Neptune service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_timestream_service\":\n\t\targs := []string{\"timestream-query\", \"list-databases\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Timestream service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"timestream-query\", \"list-databases\", \"--output\", \"json\", \"--query\", \"length(Databases)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Timestream service is available. Database count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_inspector_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector service is available\", nil\n\n\tcase \"check_macie_service\":\n\t\targs := []string{\"macie2\", \"get-macie-session\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Macie service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Macie service is available\", nil\n\n\tcase \"check_backup_service\":\n\t\targs := []string{\"backup\", \"list-backup-vaults\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Backup service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"backup\", \"list-backup-vaults\", \"--output\", \"json\", \"--query\", \"length(BackupVaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Backup service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_organizations_service\":\n\t\targs := []string{\"organizations\", \"describe-organization\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Organizations service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Organizations service is available\", nil\n\n\tcase \"check_quicksight_service\":\n\t\targs := []string{\"quicksight\", \"list-users\", \"--aws-account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--namespace\", \"default\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QuickSight service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ QuickSight service is available\", nil\n\n\tcase \"check_msk_service\":\n\t\targs := []string{\"kafka\", \"list-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MSK service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kafka\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(ClusterInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MSK service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transitgateway_service\":\n\t\targs := []string{\"ec2\", \"describe-transit-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transit Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-transit-gateways\", \"--output\", \"json\", \"--query\", \"length(TransitGateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transit Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_securityhub_service\":\n\t\targs := []string{\"securityhub\", \"get-enabled-standards\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Security Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Security Hub service is available\", nil\n\n\tcase \"check_servicecatalog_service\":\n\t\targs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Service Catalog service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"servicecatalog\", \"list-portfolios\", \"--output\", \"json\", \"--query\", \"length(PortfolioDetails)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Service Catalog service is available. Portfolio count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lakeformation_service\":\n\t\targs := []string{\"lakeformation\", \"list-permissions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lake Formation service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lake Formation service is available\", nil\n\n\tcase \"check_mq_service\":\n\t\targs := []string{\"mq\", \"list-brokers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MQ service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mq\", \"list-brokers\", \"--output\", \"json\", \"--query\", \"length(BrokerSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MQ service is available. Broker count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_fsx_service\":\n\t\targs := []string{\"fsx\", \"describe-file-systems\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FSx service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"fsx\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ FSx service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directconnect_service\":\n\t\targs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Direct Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"directconnect\", \"describe-connections\", \"--output\", \"json\", \"--query\", \"length(connections)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Direct Connect service is available. Connection count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dms_service\":\n\t\targs := []string{\"dms\", \"describe-replication-instances\", \"--max-records\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DMS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dms\", \"describe-replication-instances\", \"--output\", \"json\", \"--query\", \"length(ReplicationInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DMS service is available. Replication instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_globalaccelerator_service\":\n\t\targs := []string{\"globalaccelerator\", \"list-accelerators\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Global Accelerator service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"globalaccelerator\", \"list-accelerators\", \"--output\", \"json\", \"--query\", \"length(Accelerators)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Global Accelerator service is available. Accelerator count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workspaces_service\":\n\t\targs := []string{\"workspaces\", \"describe-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkSpaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"workspaces\", \"describe-workspaces\", \"--output\", \"json\", \"--query\", \"length(Workspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ WorkSpaces service is available. Workspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_connect_service\":\n\t\targs := []string{\"connect\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Connect service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"connect\", \"list-instances\", \"--output\", \"json\", \"--query\", \"length(InstanceSummaryList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Connect service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iot_service\":\n\t\targs := []string{\"iot\", \"list-things\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iot\", \"list-things\", \"--output\", \"json\", \"--query\", \"length(things)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT service is available. Thing count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeartifact_service\":\n\t\targs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeArtifact service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"codeartifact\", \"list-domains\", \"--output\", \"json\", \"--query\", \"length(domains)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeArtifact service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codeguru_service\":\n\t\targs := []string{\"codeguru-reviewer\", \"list-repository-associations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeGuru service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeGuru service is available\", nil\n\n\tcase \"check_devicefarm_service\":\n\t\targs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Device Farm service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"devicefarm\", \"list-projects\", \"--output\", \"json\", \"--query\", \"length(projects)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Device Farm service is available. Project count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_pinpoint_service\":\n\t\targs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Pinpoint service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"pinpoint\", \"get-apps\", \"--output\", \"json\", \"--query\", \"length(ApplicationsResponse.Item)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Pinpoint service is available. App count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_storagegateway_service\":\n\t\targs := []string{\"storagegateway\", \"list-gateways\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Storage Gateway service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"storagegateway\", \"list-gateways\", \"--output\", \"json\", \"--query\", \"length(Gateways)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Storage Gateway service is available. Gateway count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_transferfamily_service\":\n\t\targs := []string{\"transfer\", \"list-servers\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Transfer Family service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"transfer\", \"list-servers\", \"--output\", \"json\", \"--query\", \"length(Servers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Transfer Family service is available. Server count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_appmesh_service\":\n\t\targs := []string{\"appmesh\", \"list-meshes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ App Mesh service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"appmesh\", \"list-meshes\", \"--output\", \"json\", \"--query\", \"length(meshes)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ App Mesh service is available. Mesh count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_privatelink_service\":\n\t\targs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ PrivateLink service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-vpc-endpoints\", \"--output\", \"json\", \"--query\", \"length(VpcEndpoints)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ PrivateLink service is available. Endpoint count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_controltower_service\":\n\t\targs := []string{\"controltower\", \"list-enabled-controls\", \"--target-identifier\", \"ou-root\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Control Tower service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Control Tower service is available\", nil\n\n\tcase \"check_licensemanager_service\":\n\t\targs := []string{\"license-manager\", \"list-licenses\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ License Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ License Manager service is available\", nil\n\n\tcase \"check_resourcegroups_service\":\n\t\targs := []string{\"resource-groups\", \"list-groups\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Groups service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"resource-groups\", \"list-groups\", \"--output\", \"json\", \"--query\", \"length(GroupIdentifiers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Resource Groups service is available. Group count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_directoryservice_service\":\n\t\targs := []string{\"ds\", \"describe-directories\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Directory Service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ds\", \"describe-directories\", \"--output\", \"json\", \"--query\", \"length(DirectoryDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Directory Service is available. Directory count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sso_service\":\n\t\targs := []string{\"sso-admin\", \"list-instances\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SSO service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ SSO service is available\", nil\n\n\tcase \"check_privateca_service\":\n\t\targs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Private CA service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"acm-pca\", \"list-certificate-authorities\", \"--output\", \"json\", \"--query\", \"length(CertificateAuthorities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Private CA service is available. CA count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_memorydb_service\":\n\t\targs := []string{\"memorydb\", \"describe-clusters\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MemoryDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"memorydb\", \"describe-clusters\", \"--output\", \"json\", \"--query\", \"length(Clusters)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MemoryDB service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_keyspaces_service\":\n\t\targs := []string{\"cassandra\", \"list-keyspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Keyspaces service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cassandra\", \"list-keyspaces\", \"--output\", \"json\", \"--query\", \"length(keyspaces)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Keyspaces service is available. Keyspace count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_qldb_service\":\n\t\targs := []string{\"qldb\", \"list-ledgers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ QLDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"qldb\", \"list-ledgers\", \"--output\", \"json\", \"--query\", \"length(Ledgers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ QLDB service is available. Ledger count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_swf_service\":\n\t\targs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SWF service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"swf\", \"list-domains\", \"--registration-status\", \"REGISTERED\", \"--output\", \"json\", \"--query\", \"length(domainInfos)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SWF service is available. Domain count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_costexplorer_service\":\n\t\targs := []string{\"ce\", \"get-usage-and-costs\", \"--time-period\", \"Start=2025-08-01,End=2025-08-02\", \"--granularity\", \"DAILY\", \"--metrics\", \"BlendedCost\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost Explorer service is available\", nil\n\n\tcase \"check_budgets_service\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--account-id\", \"$(aws sts get-caller-identity --query Account --output text)\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Budgets service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Budgets service is available\", nil\n\n\tcase \"check_datasync_service\":\n\t\targs := []string{\"datasync\", \"list-tasks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DataSync service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datasync\", \"list-tasks\", \"--output\", \"json\", \"--query\", \"length(Tasks)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DataSync service is available. Task count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_migrationhub_service\":\n\t\targs := []string{\"mgh\", \"list-created-artifacts\", \"--progress-update-stream\", \"test\", \"--migration-task-name\", \"test\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Migration Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Migration Hub service is available\", nil\n\n\tcase \"check_elasticbeanstalk_service\":\n\t\targs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Beanstalk service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elasticbeanstalk\", \"describe-applications\", \"--output\", \"json\", \"--query\", \"length(Applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Beanstalk service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_cloudshell_service\":\n\t\targs := []string{\"cloudshell\", \"describe-environments\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudShell service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudShell service is available\", nil\n\n\tcase \"check_autoscaling_service\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Auto Scaling service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"json\", \"--query\", \"length(AutoScalingGroups)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Auto Scaling service is available. ASG count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elb_service\":\n\t\targs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELB Classic service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancerDescriptions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELB Classic service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elbv2_service\":\n\t\targs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ELBv2 (ALB/NLB) service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"json\", \"--query\", \"length(LoadBalancers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ELBv2 (ALB/NLB) service is available. Load balancer count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_efs_service\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EFS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"json\", \"--query\", \"length(FileSystems)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EFS service is available. File system count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_glacier_service\":\n\t\targs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Glacier service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"glacier\", \"list-vaults\", \"--account-id\", \"-\", \"--output\", \"json\", \"--query\", \"length(VaultList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Glacier service is available. Vault count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_lightsail_service\":\n\t\targs := []string{\"lightsail\", \"get-instances\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lightsail service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"lightsail\", \"get-instances\", \"--output\", \"json\", \"--query\", \"length(instances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lightsail service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ses_service\":\n\t\targs := []string{\"ses\", \"list-identities\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SES service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ses\", \"list-identities\", \"--output\", \"json\", \"--query\", \"length(Identities)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SES service is available. Identity count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codedeploy_service\":\n\t\targs := []string{\"deploy\", \"list-applications\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeDeploy service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"deploy\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ CodeDeploy service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_codestar_service\":\n\t\targs := []string{\"codestar\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CodeStar service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CodeStar service is available\", nil\n\n\tcase \"check_cloud9_service\":\n\t\targs := []string{\"cloud9\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cloud9 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"cloud9\", \"list-environments\", \"--output\", \"json\", \"--query\", \"length(environmentIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Cloud9 service is available. Environment count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_emrserverless_service\":\n\t\targs := []string{\"emr-serverless\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EMR Serverless service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"emr-serverless\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(applications)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EMR Serverless service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_datapipeline_service\":\n\t\targs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Pipeline service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"datapipeline\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(pipelineIdList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Data Pipeline service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_firehose_service\":\n\t\targs := []string{\"firehose\", \"list-delivery-streams\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Firehose service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"firehose\", \"list-delivery-streams\", \"--output\", \"json\", \"--query\", \"length(DeliveryStreamNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Firehose service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisanalytics_service\":\n\t\targs := []string{\"kinesisanalytics\", \"list-applications\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisanalytics\", \"list-applications\", \"--output\", \"json\", \"--query\", \"length(ApplicationSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Analytics service is available. Application count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_elastictranscoder_service\":\n\t\targs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Elastic Transcoder service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"elastictranscoder\", \"list-pipelines\", \"--output\", \"json\", \"--query\", \"length(Pipelines)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Elastic Transcoder service is available. Pipeline count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_kinesisvideo_service\":\n\t\targs := []string{\"kinesisvideo\", \"list-streams\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Kinesis Video service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"kinesisvideo\", \"list-streams\", \"--output\", \"json\", \"--query\", \"length(StreamInfoList)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Kinesis Video service is available. Stream count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediaconvert_service\":\n\t\targs := []string{\"mediaconvert\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaConvert service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaConvert service is available\", nil\n\n\tcase \"check_medialive_service\":\n\t\targs := []string{\"medialive\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaLive service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"medialive\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(Channels)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaLive service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotanalytics_service\":\n\t\targs := []string{\"iotanalytics\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Analytics service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotanalytics\", \"list-channels\", \"--output\", \"json\", \"--query\", \"length(channelSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT Analytics service is available. Channel count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_iotevents_service\":\n\t\targs := []string{\"iotevents\", \"list-detector-models\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Events service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Events service is available\", nil\n\n\tcase \"check_iotsitewise_service\":\n\t\targs := []string{\"iotsitewise\", \"list-assets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT SiteWise service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"iotsitewise\", \"list-assets\", \"--output\", \"json\", \"--query\", \"length(assetSummaries)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ IoT SiteWise service is available. Asset count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_greengrass_service\":\n\t\targs := []string{\"greengrass\", \"list-core-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Greengrass service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Greengrass service is available\", nil\n\n\tcase \"check_auditmanager_service\":\n\t\targs := []string{\"auditmanager\", \"get-assessments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Audit Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Audit Manager service is available\", nil\n\n\tcase \"check_wellarchitected_service\":\n\t\targs := []string{\"wellarchitected\", \"list-workloads\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Well-Architected Tool service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Well-Architected Tool service is available\", nil\n\n\tcase \"check_support_service\":\n\t\targs := []string{\"support\", \"describe-cases\", \"--include-resolved-cases\", \"false\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Support service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Support (Trusted Advisor) service is available\", nil\n\n\tcase \"check_braket_service\":\n\t\targs := []string{\"braket\", \"search-devices\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Braket (Quantum) service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Braket (Quantum) service is available\", nil\n\n\tcase \"check_robomaker_service\":\n\t\targs := []string{\"robomaker\", \"list-robots\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RoboMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ RoboMaker service is available\", nil\n\n\tcase \"check_groundstation_service\":\n\t\targs := []string{\"groundstation\", \"list-ground-stations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Ground Station service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Ground Station service is available\", nil\n\n\tcase \"check_gamelift_service\":\n\t\targs := []string{\"gamelift\", \"list-fleets\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ GameLift service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"gamelift\", \"list-fleets\", \"--output\", \"json\", \"--query\", \"length(FleetIds)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ GameLift service is available. Fleet count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_workmail_service\":\n\t\targs := []string{\"workmail\", \"list-organizations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkMail service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkMail service is available\", nil\n\n\tcase \"check_workdocs_service\":\n\t\targs := []string{\"workdocs\", \"describe-users\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ WorkDocs service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ WorkDocs service is available\", nil\n\n\tcase \"check_chime_service\":\n\t\targs := []string{\"chime\", \"list-accounts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chime service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chime service is available\", nil\n\n\tcase \"check_mediapackage_service\":\n\t\targs := []string{\"mediapackage\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaPackage service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaPackage service is available\", nil\n\n\tcase \"check_mediastore_service\":\n\t\targs := []string{\"mediastore\", \"list-containers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaStore service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"mediastore\", \"list-containers\", \"--output\", \"json\", \"--query\", \"length(Containers)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ MediaStore service is available. Container count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_mediatailor_service\":\n\t\targs := []string{\"mediatailor\", \"list-playback-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ MediaTailor service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ MediaTailor service is available\", nil\n\n\tcase \"check_ivs_service\":\n\t\targs := []string{\"ivs\", \"list-channels\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Interactive Video Service (IVS) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Interactive Video Service (IVS) is available\", nil\n\n\tcase \"check_appflow_service\":\n\t\targs := []string{\"appflow\", \"list-flows\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ AppFlow service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ AppFlow service is available\", nil\n\n\tcase \"check_cleanrooms_service\":\n\t\targs := []string{\"cleanrooms\", \"list-collaborations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Clean Rooms service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Clean Rooms service is available\", nil\n\n\tcase \"check_cloudsearch_service\":\n\t\targs := []string{\"cloudsearch\", \"list-domain-names\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudSearch service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudSearch service is available\", nil\n\n\tcase \"check_dataexchange_service\":\n\t\targs := []string{\"dataexchange\", \"list-data-sets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Data Exchange service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Data Exchange service is available\", nil\n\n\tcase \"check_finspace_service\":\n\t\targs := []string{\"finspace\", \"list-environments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ FinSpace service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ FinSpace service is available\", nil\n\n\tcase \"check_forecast_service\":\n\t\targs := []string{\"forecast\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Forecast service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Forecast service is available\", nil\n\n\tcase \"check_frauddetector_service\":\n\t\targs := []string{\"frauddetector\", \"get-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Fraud Detector service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Fraud Detector service is available\", nil\n\n\tcase \"check_lookoutequipment_service\":\n\t\targs := []string{\"lookoutequipment\", \"list-datasets\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Equipment service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Equipment service is available\", nil\n\n\tcase \"check_lookoutmetrics_service\":\n\t\targs := []string{\"lookoutmetrics\", \"list-anomaly-detectors\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Metrics service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Metrics service is available\", nil\n\n\tcase \"check_lookoutvision_service\":\n\t\targs := []string{\"lookoutvision\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lookout for Vision service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Lookout for Vision service is available\", nil\n\n\tcase \"check_monitron_service\":\n\t\targs := []string{\"monitron\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Monitron service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Monitron service is available\", nil\n\n\tcase \"check_detective_service\":\n\t\targs := []string{\"detective\", \"list-graphs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Detective service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Detective service is available\", nil\n\n\tcase \"check_signer_service\":\n\t\targs := []string{\"signer\", \"list-signing-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Signer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Signer service is available\", nil\n\n\tcase \"check_artifact_service\":\n\t\targs := []string{\"artifact\", \"list-reports\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Artifact service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Artifact service is available\", nil\n\n\tcase \"check_chatbot_service\":\n\t\targs := []string{\"chatbot\", \"describe-slack-channel-configurations\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Chatbot service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Chatbot service is available\", nil\n\n\tcase \"check_computeoptimizer_service\":\n\t\targs := []string{\"compute-optimizer\", \"get-enrollment-status\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Compute Optimizer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Compute Optimizer service is available\", nil\n\n\tcase \"check_launchwizard_service\":\n\t\targs := []string{\"launch-wizard\", \"list-deployments\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Launch Wizard service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Launch Wizard service is available\", nil\n\n\tcase \"check_managedservices_service\":\n\t\t// Note: Managed Services doesn't have public CLI operations\n\t\treturn \"❌ Managed Services has no public CLI operations available\", nil\n\n\tcase \"check_proton_service\":\n\t\targs := []string{\"proton\", \"list-services\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Proton service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Proton service is available\", nil\n\n\tcase \"check_resiliencehub_service\":\n\t\targs := []string{\"resiliencehub\", \"list-apps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resilience Hub service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resilience Hub service is available\", nil\n\n\tcase \"check_resourceexplorer_service\":\n\t\targs := []string{\"resource-explorer-2\", \"list-indexes\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Resource Explorer service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Resource Explorer service is available\", nil\n\n\tcase \"check_snowball_service\":\n\t\targs := []string{\"snowball\", \"list-jobs\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Snowball service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Snowball service is available\", nil\n\n\tcase \"check_mgn_service\":\n\t\targs := []string{\"mgn\", \"describe-source-servers\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Migration Service (MGN) not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Migration Service (MGN) is available\", nil\n\n\tcase \"check_m2_service\":\n\t\targs := []string{\"m2\", \"list-applications\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Mainframe Modernization service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Mainframe Modernization service is available\", nil\n\n\tcase \"check_discovery_service\":\n\t\targs := []string{\"discovery\", \"describe-agents\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Discovery Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Discovery Service is available\", nil\n\n\tcase \"check_cur_service\":\n\t\targs := []string{\"cur\", \"describe-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Cost and Usage Report service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Cost and Usage Report service is available\", nil\n\n\tcase \"check_applicationcostprofiler_service\":\n\t\targs := []string{\"application-cost-profiler\", \"list-report-definitions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Application Cost Profiler service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Application Cost Profiler service is available\", nil\n\n\tcase \"check_managedblockchain_service\":\n\t\targs := []string{\"managedblockchain\", \"list-networks\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Managed Blockchain service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Managed Blockchain service is available\", nil\n\n\tcase \"check_alexaforbusiness_service\":\n\t\targs := []string{\"alexaforbusiness\", \"list-skills\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Alexa for Business service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Alexa for Business service is available\", nil\n\n\tcase \"check_outposts_service\":\n\t\targs := []string{\"outposts\", \"list-outposts\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Outposts service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Outposts service is available\", nil\n\n\tcase \"check_serverlessrepo_service\":\n\t\targs := []string{\"serverlessrepo\", \"list-applications\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Serverless Application Repository not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Serverless Application Repository is available\", nil\n\n\tcase \"check_wavelength_service\":\n\t\targs := []string{\"ec2\", \"describe-carrier-gateways\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Wavelength service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Wavelength service is available\", nil\n\n\tcase \"check_redhatopenshiftaws_service\":\n\t\t// ROSA uses the 'rosa' CLI, not AWS CLI\n\t\treturn \"❌ Red Hat OpenShift on AWS uses separate 'rosa' CLI tool\", nil\n\n\tcase \"check_location_service\":\n\t\targs := []string{\"location\", \"list-maps\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Location Service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Location Service is available\", nil\n\n\tcase \"check_iot1click_service\":\n\t\targs := []string{\"iot1click-projects\", \"list-projects\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT 1-Click service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT 1-Click service is available\", nil\n\n\tcase \"check_iotfleetwise_service\":\n\t\targs := []string{\"iotfleetwise\", \"list-campaigns\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT FleetWise service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT FleetWise service is available\", nil\n\n\tcase \"check_iotthingsgraph_service\":\n\t\targs := []string{\"iotthingsgraph\", \"list-flow-templates\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT Things Graph service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT Things Graph service is available\", nil\n\n\tcase \"check_iottwinmaker_service\":\n\t\targs := []string{\"iottwinmaker\", \"list-workspaces\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ IoT TwinMaker service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ IoT TwinMaker service is available\", nil\n\n\tcase \"check_cloudhsm_service\":\n\t\targs := []string{\"cloudhsm\", \"list-hapgs\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ CloudHSM service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ CloudHSM service is available\", nil\n\n\tcase \"check_fms_service\":\n\t\targs := []string{\"fms\", \"list-policies\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Firewall Manager service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Firewall Manager service is available\", nil\n\n\tcase \"check_inspector2_service\":\n\t\targs := []string{\"inspector2\", \"list-findings\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Inspector v2 service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Inspector v2 service is available\", nil\n\n\tcase \"check_networkfirewall_service\":\n\t\targs := []string{\"network-firewall\", \"list-firewalls\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Network Firewall service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"network-firewall\", \"list-firewalls\", \"--output\", \"json\", \"--query\", \"length(Firewalls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Network Firewall service is available. Firewall count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_shield_service\":\n\t\targs := []string{\"shield\", \"list-subscriptions\", \"--max-results\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Shield service not available or no access\", nil\n\t\t}\n\t\treturn \"✅ Shield service is available\", nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// Use the existing log functionality\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// INFRASTRUCTURE DISCOVERY operations\n\tcase \"discover_all_active_services\":\n\t\treturn c.discoverAllActiveServices(ctx, profile)\n\n\tcase \"get_infrastructure_overview\":\n\t\treturn c.getInfrastructureOverview(ctx, profile)\n\n\tcase \"check_all_services_parallel\":\n\t\treturn c.checkAllServicesParallel(ctx, profile)\n\n\t// TERRAFORM INTEGRATION operations\n\tcase \"get_terraform_outputs\":\n\t\treturn c.getTerraformOutputs(ctx, profile)\n\n\tcase \"get_terraform_state_summary\":\n\t\treturn c.getTerraformStateSummary(ctx, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Command failed: %v, output: %s\\n\", err, string(output))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"✅ Command output (%d bytes): %s\\n\", len(output), string(output))\n\t}\n\treturn string(output), nil\n}\n\n// GetLLMAnalysisPrompt returns the prompt for LLM to analyze what AWS operations are needed\nfunc GetLLMAnalysisPrompt(question string) string {\n\treturn fmt.Sprintf(`Analyze this user query and determine what AWS operations would be needed to answer it accurately. \n\nUser Query: \"%s\"\n\nAvailable AWS READ-ONLY operations (all are safe and never modify/delete anything):\n\nINFRASTRUCTURE DISCOVERY (New Enhanced Operations):\n- discover_all_active_services: Automatically discover all active AWS services by running service checks in parallel\n- get_infrastructure_overview: Get a comprehensive overview of the entire infrastructure across all services\n- check_all_services_parallel: Run all service availability checks in parallel to map the infrastructure\n\nTERRAFORM INTEGRATION:\n- get_terraform_outputs: Get terraform outputs from the configured workspace\n- get_terraform_state_summary: Get a summary of terraform state resources\n\nSERVICE EXISTENCE CHECKS (Quick checks to see if services exist and their basic counts):\n- check_sqs_service: Check if SQS service is available and count queues\n- check_eventbridge_service: Check if EventBridge service is available and count rules\n- check_lambda_service: Check if Lambda service is available and count functions\n- check_sns_service: Check if SNS service is available and count topics\n- check_dynamodb_service: Check if DynamoDB service is available and count tables\n- check_s3_service: Check if S3 service is available and count buckets\n- check_rds_service: Check if RDS service is available and count instances\n- check_ec2_service: Check if EC2 service is available and count instances\n- check_ecs_service: Check if ECS service is available and count clusters\n- check_ecr_service: Check if ECR service is available and count repositories\n\nCOMPUTE:\n- list_ec2_instances: List EC2 instances with state, type, and details\n- describe_instance: Get detailed info about a specific EC2 instance\n- list_ecs_clusters: List ECS clusters and their running services/tasks\n- describe_ecs_service: Get details about a specific ECS service\n- list_batch_jobs: List AWS Batch jobs and their status\n- list_auto_scaling_groups: List Auto Scaling Groups with instance counts and capacity\n- describe_auto_scaling_group: Get detailed ASG configuration and instances\n- list_launch_templates: List EC2 Launch Templates and their versions\n- describe_launch_template: Get detailed Launch Template configuration\n\nSERVERLESS:\n- list_lambda_functions: List Lambda functions with runtime and last modified\n- describe_lambda_function: Get detailed config for a specific Lambda function\n- list_lambda_layers: List Lambda layers available\n\nCONTAINER SERVICES:\n- list_ecr_repositories: List ECR repositories with URIs and creation dates\n- describe_ecr_repository: Get images and details for a specific ECR repository\n- list_eks_clusters: List EKS Kubernetes clusters with status and details\n- describe_eks_cluster: Get detailed EKS cluster configuration\n\nSTORAGE:\n- list_s3_buckets: List S3 buckets with creation dates and regions\n- describe_s3_bucket: Get details about a specific S3 bucket (size, objects, etc.)\n- list_ebs_volumes: List EBS volumes and their attachments, size, type\n- describe_ebs_volume: Get detailed info about a specific EBS volume\n- list_efs_filesystems: List EFS file systems with performance modes\n- describe_efs_filesystem: Get detailed EFS configuration and mount targets\n\nDATABASE:\n- list_rds_instances: List RDS database instances with status and config\n- describe_rds_instance: Get detailed info about a specific RDS instance\n- list_rds_clusters: List RDS Aurora clusters with engine and status\n- list_dynamodb_tables: List DynamoDB tables\n- describe_dynamodb_table: Get detailed DynamoDB table schema and settings\n\nNETWORKING:\n- list_vpcs: List VPCs and their CIDR blocks\n- list_subnets: List subnets across VPCs\n- list_security_groups: List security groups and their rules\n- describe_load_balancers: List and describe load balancers (ALB/NLB/CLB)\n- list_route_tables: List route tables and their routes\n\nMESSAGE QUEUING & EVENTS:\n- list_sqs_queues: List SQS queues with URLs and attributes\n- describe_sqs_queue: Get detailed SQS queue configuration and metrics\n- list_sns_topics: List SNS topics and their ARNs\n- describe_sns_topic: Get SNS topic configuration and subscriptions\n- list_eventbridge_rules: List EventBridge rules with schedules and targets\n- list_eventbridge_buses: List custom EventBridge event buses\n\nMONITORING & LOGS:\n- get_recent_logs: Get recent CloudWatch logs and errors\n- list_cloudwatch_alarms: List CloudWatch alarms and their status\n- describe_cloudwatch_metrics: Get CloudWatch metrics for resources\n- list_log_groups: List CloudWatch log groups\n\nSECURITY & IAM:\n- list_iam_roles: List IAM roles (names only, no sensitive data)\n- list_iam_users: List IAM users (names only, no sensitive data)\n- describe_security_groups: Get security group rules and associations\n- list_kms_keys: List KMS encryption keys\n- describe_kms_key: Get KMS key details and policies\n- list_acm_certificates: List SSL/TLS certificates with status\n- describe_acm_certificate: Get certificate details and validation\n- list_waf_webacls: List WAF Web ACLs for regional and CloudFront\n\nDEVOPS & CI/CD:\n- list_codebuild_projects: List CodeBuild projects with build history\n- list_codepipelines: List CodePipeline pipelines with status\n- describe_codepipeline: Get detailed pipeline configuration and stages\n- list_codecommit_repositories: List CodeCommit Git repositories\n\nANALYTICS & BIG DATA:\n- list_kinesis_streams: List Kinesis data streams\n- describe_kinesis_stream: Get Kinesis stream shards and throughput\n- list_glue_jobs: List AWS Glue ETL jobs and schedules\n- list_glue_databases: List Glue Data Catalog databases\n- list_emr_clusters: List EMR big data clusters\n\nMACHINE LEARNING:\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n\nCACHING:\n- list_elasticache_clusters: List ElastiCache Redis/Memcached clusters\n- describe_elasticache_cluster: Get cache cluster configuration and nodes\n\nAPPLICATION INTEGRATION:\n- list_step_functions: List Step Functions state machines\n- describe_step_function: Get Step Function workflow definition\n\nCOST & BILLING:\n- get_cost_and_usage: Get cost information and usage metrics\n- list_budgets: List AWS Budgets and spending alerts\n\nAI/ML SERVICES:\n- list_bedrock_foundation_models: List available Bedrock foundation models\n- list_bedrock_custom_models: List custom Bedrock models\n- list_bedrock_agents: List Bedrock agents\n- list_bedrock_knowledge_bases: List Bedrock knowledge bases\n- list_bedrock_guardrails: List Bedrock guardrails\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n- list_comprehend_jobs: List Comprehend analysis jobs\n- list_textract_jobs: List Textract document analysis jobs\n- list_rekognition_collections: List Rekognition face collections\n\nOTHER SERVICES:\n- list_api_gateways: List API Gateway REST and HTTP APIs\n- list_cloudfront_distributions: List CloudFront distributions\n- list_route53_zones: List Route53 hosted zones\n- list_secrets: List AWS Secrets Manager secrets (names only)\n- list_ssm_parameters: List Systems Manager parameters (names only)\n\nRespond with ONLY a JSON object in this format:\n{\n \"operations\": [\n {\n \"operation\": \"operation_name\",\n \"reason\": \"why this operation is needed\",\n \"parameters\": {\"key\": \"value\"}\n }\n ],\n \"analysis\": \"brief explanation of what the user wants to know\"\n}\n\nIf no AWS operations are needed, return: {\"operations\": [], \"analysis\": \"explanation\"}`, question)\n}\n\n// discoverAllActiveServices discovers all active AWS services by running service checks in parallel\nfunc (c *Client) discoverAllActiveServices(ctx context.Context, profile *AIProfile) (string, error) {\n\tserviceChecks := []string{\n\t\t\"check_ec2_service\",\n\t\t\"check_ecs_service\",\n\t\t\"check_lambda_service\",\n\t\t\"check_rds_service\",\n\t\t\"check_s3_service\",\n\t\t\"check_dynamodb_service\",\n\t\t\"check_sqs_service\",\n\t\t\"check_sns_service\",\n\t\t\"check_eventbridge_service\",\n\t\t\"check_ecr_service\",\n\t\t\"check_elasticsearch_service\",\n\t\t\"check_opensearch_service\",\n\t\t\"check_eks_service\",\n\t\t\"check_elasticache_service\",\n\t\t\"check_redshift_service\",\n\t\t\"check_kinesis_service\",\n\t\t\"check_cloudformation_service\",\n\t\t\"check_kms_service\",\n\t\t\"check_secretsmanager_service\",\n\t\t\"check_route53_service\",\n\t\t\"check_cloudfront_service\",\n\t\t\"check_apigateway_service\",\n\t\t\"check_bedrock_service\",\n\t\t\"check_codecommit_service\",\n\t\t\"check_codebuild_service\",\n\t\t\"check_codepipeline_service\",\n\t\t\"check_sagemaker_service\",\n\t\t\"check_glue_service\",\n\t\t\"check_athena_service\",\n\t\t\"check_emr_service\",\n\t\t\"check_stepfunctions_service\",\n\t\t\"check_cloudwatch_service\",\n\t\t\"check_logs_service\",\n\t\t\"check_xray_service\",\n\t\t\"check_cognito_service\",\n\t\t\"check_wafv2_service\",\n\t\t\"check_shield_service\",\n\t\t\"check_acm_service\",\n\t\t\"check_cloudtrail_service\",\n\t\t\"check_config_service\",\n\t\t\"check_guardduty_service\",\n\t\t\"check_ssm_service\",\n\t\t\"check_batch_service\",\n\t\t\"check_appsync_service\",\n\t\t\"check_amplify_service\",\n\t\t\"check_comprehend_service\",\n\t\t\"check_textract_service\",\n\t\t\"check_rekognition_service\",\n\t\t\"check_polly_service\",\n\t\t\"check_transcribe_service\",\n\t\t\"check_translate_service\",\n\t\t\"check_personalize_service\",\n\t\t\"check_kendra_service\",\n\t\t\"check_lex_service\",\n\t\t\"check_apprunner_service\",\n\t\t\"check_documentdb_service\",\n\t\t\"check_neptune_service\",\n\t\t\"check_timestream_service\",\n\t\t\"check_inspector_service\",\n\t\t\"check_macie_service\",\n\t\t\"check_backup_service\",\n\t\t\"check_organizations_service\",\n\t\t\"check_quicksight_service\",\n\t\t\"check_msk_service\",\n\t\t\"check_transitgateway_service\",\n\t\t\"check_securityhub_service\",\n\t\t\"check_servicecatalog_service\",\n\t\t\"check_lakeformation_service\",\n\t\t\"check_mq_service\",\n\t\t\"check_fsx_service\",\n\t\t\"check_directconnect_service\",\n\t\t\"check_dms_service\",\n\t\t\"check_globalaccelerator_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_workspaces_service\",\n\t\t\"check_connect_service\",\n\t\t\"check_iot_service\",\n\t\t\"check_codeartifact_service\",\n\t\t\"check_codeguru_service\",\n\t\t\"check_devicefarm_service\",\n\t\t\"check_pinpoint_service\",\n\t\t\"check_storagegateway_service\",\n\t\t\"check_transferfamily_service\",\n\t\t\"check_appmesh_service\",\n\t\t\"check_privatelink_service\",\n\t\t\"check_controltower_service\",\n\t\t\"check_licensemanager_service\",\n\t\t\"check_resourcegroups_service\",\n\t\t\"check_directoryservice_service\",\n\t\t\"check_sso_service\",\n\t\t\"check_privateca_service\",\n\t\t\"check_memorydb_service\",\n\t\t\"check_keyspaces_service\",\n\t\t\"check_qldb_service\",\n\t\t\"check_swf_service\",\n\t\t\"check_costexplorer_service\",\n\t\t\"check_budgets_service\",\n\t\t\"check_datasync_service\",\n\t\t\"check_migrationhub_service\",\n\t\t\"check_elasticbeanstalk_service\",\n\t\t\"check_cloudshell_service\",\n\t\t\"check_autoscaling_service\",\n\t\t\"check_elb_service\",\n\t\t\"check_elbv2_service\",\n\t\t\"check_efs_service\",\n\t\t\"check_glacier_service\",\n\t\t\"check_lightsail_service\",\n\t\t\"check_ses_service\",\n\t\t\"check_codedeploy_service\",\n\t\t\"check_codestar_service\",\n\t\t\"check_cloud9_service\",\n\t\t\"check_emrserverless_service\",\n\t\t\"check_datapipeline_service\",\n\t\t\"check_firehose_service\",\n\t\t\"check_kinesisanalytics_service\",\n\t\t\"check_elastictranscoder_service\",\n\t\t\"check_kinesisvideo_service\",\n\t\t\"check_mediaconvert_service\",\n\t\t\"check_medialive_service\",\n\t\t\"check_iotanalytics_service\",\n\t\t\"check_iotevents_service\",\n\t\t\"check_iotsitewise_service\",\n\t\t\"check_greengrass_service\",\n\t\t\"check_auditmanager_service\",\n\t\t\"check_wellarchitected_service\",\n\t\t\"check_support_service\",\n\t\t\"check_braket_service\",\n\t\t\"check_robomaker_service\",\n\t\t\"check_groundstation_service\",\n\t\t\"check_gamelift_service\",\n\t\t\"check_workmail_service\",\n\t\t\"check_workdocs_service\",\n\t\t\"check_chime_service\",\n\t\t\"check_mediapackage_service\",\n\t\t\"check_mediastore_service\",\n\t\t\"check_mediatailor_service\",\n\t\t\"check_ivs_service\",\n\t\t\"check_appflow_service\",\n\t\t\"check_cleanrooms_service\",\n\t\t\"check_cloudsearch_service\",\n\t\t\"check_dataexchange_service\",\n\t\t\"check_finspace_service\",\n\t\t\"check_forecast_service\",\n\t\t\"check_frauddetector_service\",\n\t\t\"check_lookoutequipment_service\",\n\t\t\"check_lookoutmetrics_service\",\n\t\t\"check_lookoutvision_service\",\n\t\t\"check_monitron_service\",\n\t\t\"check_detective_service\",\n\t\t\"check_signer_service\",\n\t\t\"check_artifact_service\",\n\t\t\"check_chatbot_service\",\n\t\t\"check_computeoptimizer_service\",\n\t\t\"check_launchwizard_service\",\n\t\t\"check_managedservices_service\",\n\t\t\"check_proton_service\",\n\t\t\"check_resiliencehub_service\",\n\t\t\"check_resourceexplorer_service\",\n\t\t\"check_snowball_service\",\n\t\t\"check_mgn_service\",\n\t\t\"check_m2_service\",\n\t\t\"check_discovery_service\",\n\t\t\"check_cur_service\",\n\t\t\"check_applicationcostprofiler_service\",\n\t\t\"check_managedblockchain_service\",\n\t\t\"check_alexaforbusiness_service\",\n\t\t\"check_outposts_service\",\n\t\t\"check_serverlessrepo_service\",\n\t\t\"check_wavelength_service\",\n\t\t\"check_redhatopenshiftaws_service\",\n\t\t\"check_location_service\",\n\t\t\"check_iot1click_service\",\n\t\t\"check_iotfleetwise_service\",\n\t\t\"check_iotthingsgraph_service\",\n\t\t\"check_iottwinmaker_service\",\n\t\t\"check_cloudhsm_service\",\n\t\t\"check_fms_service\",\n\t\t\"check_inspector2_service\",\n\t\t\"check_networkfirewall_service\",\n\t\t\"check_shield_service\",\n\t}\n\n\toperations := make([]LLMOperation, len(serviceChecks))\n\tfor i, check := range serviceChecks {\n\t\toperations[i] = LLMOperation{\n\t\t\tOperation: check,\n\t\t\tReason: \"Infrastructure discovery\",\n\t\t\tParameters: make(map[string]interface{}),\n\t\t}\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// getInfrastructureOverview gets a comprehensive overview of the entire infrastructure\nfunc (c *Client) getInfrastructureOverview(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Core infrastructure operations to run in parallel\n\toverviewOps := []LLMOperation{\n\t\t{Operation: \"list_ec2_instances\", Reason: \"Get EC2 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecs_clusters\", Reason: \"Get ECS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_lambda_functions\", Reason: \"Get Lambda overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_rds_instances\", Reason: \"Get RDS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_s3_buckets\", Reason: \"Get S3 overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_dynamodb_tables\", Reason: \"Get DynamoDB overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_vpcs\", Reason: \"Get VPC overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_load_balancers\", Reason: \"Get Load Balancer overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_api_gateways\", Reason: \"Get API Gateway overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudfront_distributions\", Reason: \"Get CloudFront overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sqs_queues\", Reason: \"Get SQS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_sns_topics\", Reason: \"Get SNS overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_ecr_repositories\", Reason: \"Get ECR overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_batch_jobs\", Reason: \"Get Batch overview\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"list_cloudwatch_alarms\", Reason: \"Get monitoring overview\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, overviewOps, profile)\n}\n\n// checkAllServicesParallel runs all service availability checks in parallel\nfunc (c *Client) checkAllServicesParallel(ctx context.Context, profile *AIProfile) (string, error) {\n\tallServiceChecks := []LLMOperation{\n\t\t{Operation: \"check_ec2_service\", Reason: \"Check EC2 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecs_service\", Reason: \"Check ECS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_lambda_service\", Reason: \"Check Lambda availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_rds_service\", Reason: \"Check RDS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_s3_service\", Reason: \"Check S3 availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_dynamodb_service\", Reason: \"Check DynamoDB availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sqs_service\", Reason: \"Check SQS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_sns_service\", Reason: \"Check SNS availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_eventbridge_service\", Reason: \"Check EventBridge availability\", Parameters: make(map[string]interface{})},\n\t\t{Operation: \"check_ecr_service\", Reason: \"Check ECR availability\", Parameters: make(map[string]interface{})},\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, allServiceChecks, profile)\n}\n\n// getTerraformOutputs gets terraform outputs from the configured workspace\nfunc (c *Client) getTerraformOutputs(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform outputs: %v\", err), nil\n\t}\n\n\toutputs, err := tfClient.GetTerraformOutputs(ctx)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform outputs: %v\", err), nil\n\t}\n\n\tif len(outputs) == 0 {\n\t\treturn \"No terraform outputs available\", nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Terraform Outputs:\\n\")\n\tfor key, value := range outputs {\n\t\tresult.WriteString(fmt.Sprintf(\" %s: %v\\n\", key, value))\n\t}\n\n\treturn result.String(), nil\n}\n\n// getTerraformStateSummary gets a summary of terraform state resources\nfunc (c *Client) getTerraformStateSummary(ctx context.Context, profile *AIProfile) (string, error) {\n\t// Get default workspace\n\tworkspace := viper.GetString(\"terraform.default_workspace\")\n\tif workspace == \"\" {\n\t\tworkspace = \"dev\"\n\t}\n\n\t// Try to create terraform client\n\ttfClient, err := tfclient.NewClient(workspace)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Unable to get terraform state: %v\", err), nil\n\t}\n\n\tcontext, err := tfClient.GetRelevantContext(ctx, \"terraform state summary\")\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"❌ Failed to get terraform state: %v\", err), nil\n\t}\n\n\treturn context, nil\n}\n"}, "de3f784bebb00e38c5b7aeba1c9349b1fd21674b": {"t": 1785748550.3580472, "n": 38000, "h": "abca82e0cc833298", "c": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/spf13/viper\"\n)\n\n// LLMOperationResult represents the result of an AWS operation for LLM processing\ntype LLMOperationResult struct {\n\tOperation string\n\tResult string\n\tError error\n\tIndex int\n}\n\n// LLMOperation represents an AWS operation requested by the LLM\ntype LLMOperation struct {\n\tOperation string `json:\"operation\"`\n\tReason string `json:\"reason\"`\n\tParameters map[string]interface{} `json:\"parameters\"`\n}\n\n// LLMAnalysis represents the LLM's analysis of what AWS operations are needed\ntype LLMAnalysis struct {\n\tOperations []LLMOperation `json:\"operations\"`\n\tAnalysis string `json:\"analysis\"`\n}\n\n// AIProfile represents an AI provider configuration\ntype AIProfile struct {\n\tProvider string `mapstructure:\"provider\"`\n\tAWSProfile string `mapstructure:\"aws_profile\"`\n\tModel string `mapstructure:\"model\"`\n\tRegion string `mapstructure:\"region\"`\n\tAPIKeyEnv string `mapstructure:\"api_key_env\"`\n}\n\n// GetAIProfile returns the AI configuration for the given provider name\nfunc GetAIProfile(providerName string) (*AIProfile, error) {\n\tif providerName == \"\" {\n\t\tproviderName = viper.GetString(\"ai.default_provider\")\n\t\tif providerName == \"\" {\n\t\t\tproviderName = \"bedrock\"\n\t\t}\n\t}\n\n\tprofileKey := fmt.Sprintf(\"ai.providers.%s\", providerName)\n\tif !viper.IsSet(profileKey) {\n\t\treturn nil, fmt.Errorf(\"AI provider '%s' not found in configuration\", providerName)\n\t}\n\n\tvar profile AIProfile\n\tif err := viper.UnmarshalKey(profileKey, &profile); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse AI provider '%s': %w\", providerName, err)\n\t}\n\n\t// Set the provider name\n\tprofile.Provider = providerName\n\n\treturn &profile, nil\n}\n\n// ExecuteOperationsConcurrently executes multiple AWS operations concurrently for LLM processing\nfunc (c *Client) ExecuteOperationsConcurrently(ctx context.Context, operations []LLMOperation, aiProfile string) (string, error) {\n\tif len(operations) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// Get AI profile configuration\n\tprofile, err := GetAIProfile(aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// ExecuteOperationsWithAWSProfile executes multiple AWS operations concurrently using a direct AWS profile\nfunc (c *Client) ExecuteOperationsWithAWSProfile(ctx context.Context, operations []LLMOperation, awsProfile, region string) (string, error) {\n\tif len(operations) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// Create a temporary AI profile with the specified AWS profile\n\tprofile := &AIProfile{\n\t\tProvider: \"bedrock\", // Not used for AWS operations\n\t\tAWSProfile: awsProfile,\n\t\tRegion: region,\n\t}\n\n\treturn c.executeOperationsWithProfile(ctx, operations, profile)\n}\n\n// executeOperationsWithProfile executes operations with a given profile\nfunc (c *Client) executeOperationsWithProfile(ctx context.Context, operations []LLMOperation, profile *AIProfile) (string, error) {\n\n\t// Create channels for results\n\tresultChan := make(chan LLMOperationResult, len(operations))\n\tvar wg sync.WaitGroup\n\n\t// Execute all operations concurrently\n\tfor i, op := range operations {\n\t\twg.Add(1)\n\t\tgo func(index int, operation string, params map[string]interface{}) {\n\t\t\tdefer wg.Done()\n\t\t\tresult, err := c.executeAWSOperation(ctx, operation, params, profile)\n\t\t\tresultChan <- LLMOperationResult{\n\t\t\t\tOperation: operation,\n\t\t\t\tResult: result,\n\t\t\t\tError: err,\n\t\t\t\tIndex: index,\n\t\t\t}\n\t\t}(i, op.Operation, op.Parameters)\n\t}\n\n\t// Wait for all operations to complete\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(resultChan)\n\t}()\n\n\t// Collect results in order\n\tresults := make([]LLMOperationResult, len(operations))\n\tfor result := range resultChan {\n\t\tresults[result.Index] = result\n\t}\n\n\t// Build results string in original order\n\tvar awsResults strings.Builder\n\tfor _, result := range results {\n\t\tif result.Error != nil {\n\t\t\tawsResults.WriteString(fmt.Sprintf(\"❌ %s failed: %v\\n\", result.Operation, result.Error))\n\t\t} else {\n\t\t\tawsResults.WriteString(fmt.Sprintf(\"✅ %s:\\n%s\\n\\n\", result.Operation, result.Result))\n\t\t}\n\t}\n\n\treturn awsResults.String(), nil\n}\n\n// executeAWSOperation executes a specific AWS operation with the given parameters\nfunc (c *Client) executeAWSOperation(ctx context.Context, toolName string, input map[string]interface{}, profile *AIProfile) (string, error) {\n\t// All operations are read-only and safe - no modifications or deletions possible\n\tswitch toolName {\n\t// SERVICE EXISTENCE CHECKS - Quick checks to see if services exist/are configured\n\tcase \"check_sqs_service\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SQS service not available or no access\", nil\n\t\t}\n\t\tqueueCountArgs := []string{\"sqs\", \"list-queues\", \"--output\", \"json\", \"--query\", \"length(QueueUrls)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, queueCountArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SQS service is available. Queue count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_eventbridge_service\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EventBridge service not available or no access\", nil\n\t\t}\n\t\t// Count rules on default bus\n\t\truleArgs := []string{\"events\", \"list-rules\", \"--output\", \"json\", \"--query\", \"length(Rules)\"}\n\t\truleCount, _ := c.execAWSCLI(ctx, ruleArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EventBridge service is available. Rule count: %s\", strings.TrimSpace(ruleCount)), nil\n\n\tcase \"check_lambda_service\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ Lambda service not available or no access\", nil\n\t\t}\n\t\t// Get actual count\n\t\tcountArgs := []string{\"lambda\", \"list-functions\", \"--output\", \"json\", \"--query\", \"length(Functions)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ Lambda service is available. Function count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_sns_service\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ SNS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"sns\", \"list-topics\", \"--output\", \"json\", \"--query\", \"length(Topics)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ SNS service is available. Topic count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_dynamodb_service\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--limit\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ DynamoDB service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"json\", \"--query\", \"length(TableNames)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ DynamoDB service is available. Table count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_s3_service\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ S3 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"s3api\", \"list-buckets\", \"--output\", \"json\", \"--query\", \"length(Buckets)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ S3 service is available. Bucket count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_rds_service\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ RDS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"json\", \"--query\", \"length(DBInstances)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ RDS service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ec2_service\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ EC2 service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ec2\", \"describe-instances\", \"--output\", \"json\", \"--query\", \"length(Reservations[].Instances[])\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ EC2 service is available. Instance count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecs_service\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"❌ ECS service not available or no access\", nil\n\t\t}\n\t\tcountArgs := []string{\"ecs\", \"list-clusters\", \"--output\", \"json\", \"--query\", \"length(clusterArns)\"}\n\t\tcountResult, _ := c.execAWSCLI(ctx, countArgs, profile)\n\t\treturn fmt.Sprintf(\"✅ ECS service is available. Cluster count: %s\", strings.TrimSpace(countResult)), nil\n\n\tcase \"check_ecr_service\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--max-items\", \"1\", \"--output\", \"table\"}\n\t\tfmt.Printf(\"🔍 ECR: Checking service availability with: aws %s\\n\", strings.Join(args, \" \"))\n\t\t_, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Service check failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR service not available or no access\", nil\n\t\t}\n\t\tfmt.Printf(\"✅ ECR: Service is available, getting count...\\n\")\n\t\tcountArgs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"json\", \"--query\", \"length(repositories)\"}\n\t\tfmt.Printf(\"🔍 ECR: Getting count with: aws %s\\n\", strings.Join(countArgs, \" \"))\n\t\tcountResult, err := c.execAWSCLI(ctx, countArgs, profile)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"❌ ECR: Count query failed: %v\\n\", err)\n\t\t\treturn \"❌ ECR count query failed\", nil\n\t\t}\n\t\tfmt.Printf(\"📊 ECR: Raw count result: '%s'\\n\", countResult)\n\t\treturn fmt.Sprintf(\"✅ ECR service is available. Repository count: %s\", strings.TrimSpace(countResult)), nil\n\n\t// COMPUTE operations\n\tcase \"list_ec2_instances\":\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--output\", \"table\", \"--query\", \"Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key=='Name'].Value|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-instances\", \"--instance-ids\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ecs_clusters\":\n\t\targs := []string{\"ecs\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get services for each cluster\n\t\tserviceArgs := []string{\"ecs\", \"list-services\", \"--output\", \"table\"}\n\t\tservices, _ := c.execAWSCLI(ctx, serviceArgs, profile)\n\t\treturn fmt.Sprintf(\"Clusters:\\n%s\\n\\nServices:\\n%s\", clusters, services), nil\n\n\t// SERVERLESS operations\n\tcase \"list_lambda_functions\":\n\t\targs := []string{\"lambda\", \"list-functions\", \"--output\", \"table\", \"--query\", \"Functions[*].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_lambda_function\":\n\t\tfunctionName, ok := input[\"function_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"function_name parameter required\")\n\t\t}\n\t\targs := []string{\"lambda\", \"get-function\", \"--function-name\", functionName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// STORAGE operations\n\tcase \"list_s3_buckets\":\n\t\targs := []string{\"s3api\", \"list-buckets\", \"--output\", \"table\", \"--query\", \"Buckets[*].{Name:Name,Created:CreationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_s3_bucket\":\n\t\tbucketName, ok := input[\"bucket_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"bucket_name parameter required\")\n\t\t}\n\t\targs := []string{\"s3api\", \"head-bucket\", \"--bucket\", bucketName}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// DATABASE operations\n\tcase \"list_rds_instances\":\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--output\", \"table\", \"--query\", \"DBInstances[*].{ID:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus,Class:DBInstanceClass}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_rds_instance\":\n\t\tinstanceID, ok := input[\"instance_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"instance_id parameter required\")\n\t\t}\n\t\targs := []string{\"rds\", \"describe-db-instances\", \"--db-instance-identifier\", instanceID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_dynamodb_tables\":\n\t\targs := []string{\"dynamodb\", \"list-tables\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_dynamodb_table\":\n\t\ttableName, ok := input[\"table_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"table_name parameter required\")\n\t\t}\n\t\targs := []string{\"dynamodb\", \"describe-table\", \"--table-name\", tableName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rds_clusters\":\n\t\targs := []string{\"rds\", \"describe-db-clusters\", \"--output\", \"table\", \"--query\", \"DBClusters[*].{ID:DBClusterIdentifier,Engine:Engine,Status:Status,MultiAZ:MultiAZ}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// NETWORKING operations\n\tcase \"list_vpcs\":\n\t\targs := []string{\"ec2\", \"describe-vpcs\", \"--output\", \"table\", \"--query\", \"Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock,State:State,IsDefault:IsDefault}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_subnets\":\n\t\targs := []string{\"ec2\", \"describe-subnets\", \"--output\", \"table\", \"--query\", \"Subnets[*].{SubnetId:SubnetId,VpcId:VpcId,CidrBlock:CidrBlock,AZ:AvailabilityZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_security_groups\":\n\t\targs := []string{\"ec2\", \"describe-security-groups\", \"--output\", \"table\", \"--query\", \"SecurityGroups[*].{GroupId:GroupId,GroupName:GroupName,VpcId:VpcId,Description:Description}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_load_balancers\":\n\t\t// Try both ALB and NLB\n\t\talbArgs := []string{\"elbv2\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\talbResult, _ := c.execAWSCLI(ctx, albArgs, profile)\n\n\t\tclbArgs := []string{\"elb\", \"describe-load-balancers\", \"--output\", \"table\"}\n\t\tclbResult, _ := c.execAWSCLI(ctx, clbArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Application/Network Load Balancers:\\n%s\\n\\nClassic Load Balancers:\\n%s\", albResult, clbResult), nil\n\n\tcase \"list_route_tables\":\n\t\targs := []string{\"ec2\", \"describe-route-tables\", \"--output\", \"table\", \"--query\", \"RouteTables[*].{RouteTableId:RouteTableId,VpcId:VpcId,Main:Associations[?Main].Main|[0]}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MONITORING & LOGS operations\n\tcase \"get_recent_logs\":\n\t\t// Use the existing log functionality\n\t\treturn c.getRecentErrorLogs(ctx, \"recent logs\")\n\n\tcase \"list_cloudwatch_alarms\":\n\t\targs := []string{\"cloudwatch\", \"describe-alarms\", \"--output\", \"table\", \"--query\", \"MetricAlarms[*].{Name:AlarmName,State:StateValue,Reason:StateReason}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_log_groups\":\n\t\targs := []string{\"logs\", \"describe-log-groups\", \"--output\", \"table\", \"--query\", \"logGroups[*].{Name:logGroupName,Size:storedBytes,Retention:retentionInDays}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// SECURITY & IAM operations (safe, names only)\n\tcase \"list_iam_roles\":\n\t\targs := []string{\"iam\", \"list-roles\", \"--output\", \"table\", \"--query\", \"Roles[*].{RoleName:RoleName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_iam_users\":\n\t\targs := []string{\"iam\", \"list-users\", \"--output\", \"table\", \"--query\", \"Users[*].{UserName:UserName,CreateDate:CreateDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// OTHER SERVICES operations\n\tcase \"list_api_gateways\":\n\t\trestArgs := []string{\"apigateway\", \"get-rest-apis\", \"--output\", \"table\"}\n\t\trestResult, _ := c.execAWSCLI(ctx, restArgs, profile)\n\n\t\thttpArgs := []string{\"apigatewayv2\", \"get-apis\", \"--output\", \"table\"}\n\t\thttpResult, _ := c.execAWSCLI(ctx, httpArgs, profile)\n\n\t\treturn fmt.Sprintf(\"REST APIs:\\n%s\\n\\nHTTP APIs:\\n%s\", restResult, httpResult), nil\n\n\tcase \"list_cloudfront_distributions\":\n\t\targs := []string{\"cloudfront\", \"list-distributions\", \"--output\", \"table\", \"--query\", \"DistributionList.Items[*].{Id:Id,DomainName:DomainName,Status:Status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_route53_zones\":\n\t\targs := []string{\"route53\", \"list-hosted-zones\", \"--output\", \"table\", \"--query\", \"HostedZones[*].{Id:Id,Name:Name,Type:Config.PrivateZone}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_secrets\":\n\t\targs := []string{\"secretsmanager\", \"list-secrets\", \"--output\", \"table\", \"--query\", \"SecretList[*].{Name:Name,LastChanged:LastChangedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_ssm_parameters\":\n\t\targs := []string{\"ssm\", \"describe-parameters\", \"--output\", \"table\", \"--query\", \"Parameters[*].{Name:Name,Type:Type,LastModified:LastModifiedDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CONTAINER SERVICES operations\n\tcase \"list_ecr_repositories\":\n\t\targs := []string{\"ecr\", \"describe-repositories\", \"--output\", \"table\", \"--query\", \"repositories[*].{Name:repositoryName,URI:repositoryUri,Created:createdAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eks_clusters\":\n\t\targs := []string{\"eks\", \"list-clusters\", \"--output\", \"table\"}\n\t\tclusters, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Also get cluster details\n\t\tdetailArgs := []string{\"eks\", \"describe-cluster\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"EKS Clusters:\\n%s\\n\\nCluster Details:\\n%s\", clusters, details), nil\n\n\tcase \"describe_ecr_repository\":\n\t\trepoName, ok := input[\"repository_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"repository_name parameter required\")\n\t\t}\n\t\targs := []string{\"ecr\", \"describe-images\", \"--repository-name\", repoName, \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MESSAGE QUEUING & EVENTS operations\n\tcase \"list_sqs_queues\":\n\t\targs := []string{\"sqs\", \"list-queues\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sqs_queue\":\n\t\tqueueURL, ok := input[\"queue_url\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"queue_url parameter required\")\n\t\t}\n\t\targs := []string{\"sqs\", \"get-queue-attributes\", \"--queue-url\", queueURL, \"--attribute-names\", \"All\", \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sns_topics\":\n\t\targs := []string{\"sns\", \"list-topics\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_sns_topic\":\n\t\ttopicArn, ok := input[\"topic_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"topic_arn parameter required\")\n\t\t}\n\t\targs := []string{\"sns\", \"get-topic-attributes\", \"--topic-arn\", topicArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_rules\":\n\t\targs := []string{\"events\", \"list-rules\", \"--output\", \"table\", \"--query\", \"Rules[*].{Name:Name,State:State,ScheduleExpression:ScheduleExpression}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_eventbridge_buses\":\n\t\targs := []string{\"events\", \"list-event-buses\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AUTO SCALING & OPTIMIZATION operations\n\tcase \"list_auto_scaling_groups\":\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--output\", \"table\", \"--query\", \"AutoScalingGroups[*].{Name:AutoScalingGroupName,Instances:Instances|length,Min:MinSize,Max:MaxSize,Desired:DesiredCapacity}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_auto_scaling_group\":\n\t\tasgName, ok := input[\"asg_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"asg_name parameter required\")\n\t\t}\n\t\targs := []string{\"autoscaling\", \"describe-auto-scaling-groups\", \"--auto-scaling-group-names\", asgName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_launch_templates\":\n\t\targs := []string{\"ec2\", \"describe-launch-templates\", \"--output\", \"table\", \"--query\", \"LaunchTemplates[*].{Name:LaunchTemplateName,ID:LaunchTemplateId,Version:LatestVersionNumber,Created:CreateTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_launch_template\":\n\t\ttemplateID, ok := input[\"template_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"template_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-launch-template-versions\", \"--launch-template-id\", templateID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ADDITIONAL STORAGE operations\n\tcase \"list_ebs_volumes\":\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--output\", \"table\", \"--query\", \"Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,State:State,Device:Attachments[0].Device}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_ebs_volume\":\n\t\tvolumeID, ok := input[\"volume_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"volume_id parameter required\")\n\t\t}\n\t\targs := []string{\"ec2\", \"describe-volumes\", \"--volume-ids\", volumeID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_efs_filesystems\":\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--output\", \"table\", \"--query\", \"FileSystems[*].{ID:FileSystemId,State:LifeCycleState,Performance:PerformanceMode,Throughput:ThroughputMode}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_efs_filesystem\":\n\t\tfsID, ok := input[\"filesystem_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"filesystem_id parameter required\")\n\t\t}\n\t\targs := []string{\"efs\", \"describe-file-systems\", \"--file-system-id\", fsID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ENHANCED SECURITY operations\n\tcase \"list_kms_keys\":\n\t\targs := []string{\"kms\", \"list-keys\", \"--output\", \"table\", \"--query\", \"Keys[*].{KeyId:KeyId}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kms_key\":\n\t\tkeyID, ok := input[\"key_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"key_id parameter required\")\n\t\t}\n\t\targs := []string{\"kms\", \"describe-key\", \"--key-id\", keyID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_acm_certificates\":\n\t\targs := []string{\"acm\", \"list-certificates\", \"--output\", \"table\", \"--query\", \"CertificateSummaryList[*].{Domain:DomainName,Status:Status,Arn:CertificateArn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_acm_certificate\":\n\t\tcertArn, ok := input[\"certificate_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"certificate_arn parameter required\")\n\t\t}\n\t\targs := []string{\"acm\", \"describe-certificate\", \"--certificate-arn\", certArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_waf_webacls\":\n\t\targs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"REGIONAL\", \"--output\", \"table\"}\n\t\tregionalWAF, _ := c.execAWSCLI(ctx, args, profile)\n\n\t\tglobalArgs := []string{\"wafv2\", \"list-web-acls\", \"--scope\", \"CLOUDFRONT\", \"--output\", \"table\"}\n\t\tglobalWAF, _ := c.execAWSCLI(ctx, globalArgs, profile)\n\n\t\treturn fmt.Sprintf(\"Regional WAF ACLs:\\n%s\\n\\nCloudFront WAF ACLs:\\n%s\", regionalWAF, globalWAF), nil\n\n\t// DEVOPS & CI/CD operations\n\tcase \"list_codebuild_projects\":\n\t\targs := []string{\"codebuild\", \"list-projects\", \"--output\", \"table\"}\n\t\tprojects, err := c.execAWSCLI(ctx, args, profile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Get project details\n\t\tdetailArgs := []string{\"codebuild\", \"batch-get-projects\", \"--output\", \"json\"}\n\t\tdetails, _ := c.execAWSCLI(ctx, detailArgs, profile)\n\t\treturn fmt.Sprintf(\"CodeBuild Projects:\\n%s\\n\\nProject Details:\\n%s\", projects, details), nil\n\n\tcase \"list_codepipelines\":\n\t\targs := []string{\"codepipeline\", \"list-pipelines\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_codepipeline\":\n\t\tpipelineName, ok := input[\"pipeline_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pipeline_name parameter required\")\n\t\t}\n\t\targs := []string{\"codepipeline\", \"get-pipeline\", \"--name\", pipelineName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_codecommit_repositories\":\n\t\targs := []string{\"codecommit\", \"list-repositories\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// ANALYTICS & BIG DATA operations\n\tcase \"list_kinesis_streams\":\n\t\targs := []string{\"kinesis\", \"list-streams\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_kinesis_stream\":\n\t\tstreamName, ok := input[\"stream_name\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"stream_name parameter required\")\n\t\t}\n\t\targs := []string{\"kinesis\", \"describe-stream\", \"--stream-name\", streamName, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_jobs\":\n\t\targs := []string{\"glue\", \"get-jobs\", \"--output\", \"table\", \"--query\", \"Jobs[*].{Name:Name,Role:Role,CreatedOn:CreatedOn}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_glue_databases\":\n\t\targs := []string{\"glue\", \"get-databases\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_emr_clusters\":\n\t\targs := []string{\"emr\", \"list-clusters\", \"--output\", \"table\", \"--query\", \"Clusters[*].{ID:Id,Name:Name,State:Status.State}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// MACHINE LEARNING operations\n\tcase \"list_sagemaker_endpoints\":\n\t\targs := []string{\"sagemaker\", \"list-endpoints\", \"--output\", \"table\", \"--query\", \"Endpoints[*].{Name:EndpointName,Status:EndpointStatus,Created:CreationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_models\":\n\t\targs := []string{\"sagemaker\", \"list-models\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_sagemaker_notebook_instances\":\n\t\targs := []string{\"sagemaker\", \"list-notebook-instances\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// CACHING operations\n\tcase \"list_elasticache_clusters\":\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--output\", \"table\", \"--query\", \"CacheClusters[*].{ID:CacheClusterId,Engine:Engine,Status:CacheClusterStatus,NodeType:CacheNodeType}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_elasticache_cluster\":\n\t\tclusterID, ok := input[\"cluster_id\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cluster_id parameter required\")\n\t\t}\n\t\targs := []string{\"elasticache\", \"describe-cache-clusters\", \"--cache-cluster-id\", clusterID, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// APPLICATION INTEGRATION operations\n\tcase \"list_step_functions\":\n\t\targs := []string{\"stepfunctions\", \"list-state-machines\", \"--output\", \"table\", \"--query\", \"stateMachines[*].{Name:name,Status:status,Created:creationDate}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"describe_step_function\":\n\t\tstateMachineArn, ok := input[\"state_machine_arn\"].(string)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"state_machine_arn parameter required\")\n\t\t}\n\t\targs := []string{\"stepfunctions\", \"describe-state-machine\", \"--state-machine-arn\", stateMachineArn, \"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// COST MANAGEMENT operations\n\tcase \"get_cost_and_usage\":\n\t\t// Get cost for last 30 days\n\t\targs := []string{\"ce\", \"get-cost-and-usage\",\n\t\t\t\"--time-period\", \"Start=2024-07-01,End=2024-08-01\",\n\t\t\t\"--granularity\", \"MONTHLY\",\n\t\t\t\"--metrics\", \"BlendedCost\",\n\t\t\t\"--output\", \"json\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_budgets\":\n\t\targs := []string{\"budgets\", \"describe-budgets\", \"--output\", \"table\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// AI/ML SERVICES operations\n\tcase \"list_bedrock_foundation_models\":\n\t\targs := []string{\"bedrock\", \"list-foundation-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelId:modelId,Provider:providerName,Name:modelName,Status:modelLifecycle.status}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_custom_models\":\n\t\targs := []string{\"bedrock\", \"list-custom-models\", \"--output\", \"table\", \"--query\", \"modelSummaries[*].{ModelName:modelName,ModelArn:modelArn,Status:status,CreationTime:creationTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_agents\":\n\t\targs := []string{\"bedrock-agent\", \"list-agents\", \"--output\", \"table\", \"--query\", \"agentSummaries[*].{AgentId:agentId,AgentName:agentName,Status:agentStatus,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_knowledge_bases\":\n\t\targs := []string{\"bedrock-agent\", \"list-knowledge-bases\", \"--output\", \"table\", \"--query\", \"knowledgeBaseSummaries[*].{KnowledgeBaseId:knowledgeBaseId,Name:name,Status:status,UpdatedAt:updatedAt}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_bedrock_guardrails\":\n\t\targs := []string{\"bedrock\", \"list-guardrails\", \"--output\", \"table\", \"--query\", \"guardrails[*].{GuardrailId:id,Name:name,Status:status,Version:version}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_comprehend_jobs\":\n\t\targs := []string{\"comprehend\", \"list-sentiment-detection-jobs\", \"--output\", \"table\", \"--query\", \"SentimentDetectionJobPropertiesList[*].{JobName:JobName,Status:JobStatus,SubmitTime:SubmitTime,EndTime:EndTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_textract_jobs\":\n\t\targs := []string{\"textract\", \"list-document-analysis-jobs\", \"--output\", \"table\", \"--query\", \"DocumentAnalysisJobs[*].{JobId:JobId,Status:JobStatus,SubmissionTime:SubmissionTime,CompletionTime:CompletionTime}\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\tcase \"list_rekognition_collections\":\n\t\targs := []string{\"rekognition\", \"list-collections\", \"--output\", \"table\", \"--query\", \"CollectionIds\"}\n\t\treturn c.execAWSCLI(ctx, args, profile)\n\n\t// Fallback to existing methods for unsupported operations\n\tdefault:\n\t\treturn c.GetRelevantContext(ctx, fmt.Sprintf(\"operation %s\", toolName))\n\t}\n}\n\n// execAWSCLI executes AWS CLI commands directly\nfunc (c *Client) execAWSCLI(ctx context.Context, args []string, profile *AIProfile) (string, error) {\n\t// Build AWS CLI command\n\tcmd := exec.CommandContext(ctx, \"aws\")\n\tcmd.Args = append(cmd.Args, args...)\n\tcmd.Args = append(cmd.Args, \"--profile\", profile.AWSProfile, \"--region\", profile.Region, \"--no-cli-pager\")\n\n\tif c.debug {\n\t\tfmt.Printf(\"🚀 Executing: %s\\n\", strings.Join(cmd.Args, \" \"))\n\t}\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Command failed: %v, output: %s\\n\", err, string(output))\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"AWS CLI command failed: %w, output: %s\", err, string(output))\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"✅ Command output (%d bytes): %s\\n\", len(output), string(output))\n\t}\n\treturn string(output), nil\n}\n\n// GetLLMAnalysisPrompt returns the prompt for LLM to analyze what AWS operations are needed\nfunc GetLLMAnalysisPrompt(question string) string {\n\treturn fmt.Sprintf(`Analyze this user query and determine what AWS operations would be needed to answer it accurately. \n\nUser Query: \"%s\"\n\nAvailable AWS READ-ONLY operations (all are safe and never modify/delete anything):\n\nSERVICE EXISTENCE CHECKS (Quick checks to see if services exist and their basic counts):\n- check_sqs_service: Check if SQS service is available and count queues\n- check_eventbridge_service: Check if EventBridge service is available and count rules\n- check_lambda_service: Check if Lambda service is available and count functions\n- check_sns_service: Check if SNS service is available and count topics\n- check_dynamodb_service: Check if DynamoDB service is available and count tables\n- check_s3_service: Check if S3 service is available and count buckets\n- check_rds_service: Check if RDS service is available and count instances\n- check_ec2_service: Check if EC2 service is available and count instances\n- check_ecs_service: Check if ECS service is available and count clusters\n- check_ecr_service: Check if ECR service is available and count repositories\n\nCOMPUTE:\n- list_ec2_instances: List EC2 instances with state, type, and details\n- describe_instance: Get detailed info about a specific EC2 instance\n- list_ecs_clusters: List ECS clusters and their running services/tasks\n- describe_ecs_service: Get details about a specific ECS service\n- list_batch_jobs: List AWS Batch jobs and their status\n- list_auto_scaling_groups: List Auto Scaling Groups with instance counts and capacity\n- describe_auto_scaling_group: Get detailed ASG configuration and instances\n- list_launch_templates: List EC2 Launch Templates and their versions\n- describe_launch_template: Get detailed Launch Template configuration\n\nSERVERLESS:\n- list_lambda_functions: List Lambda functions with runtime and last modified\n- describe_lambda_function: Get detailed config for a specific Lambda function\n- list_lambda_layers: List Lambda layers available\n\nCONTAINER SERVICES:\n- list_ecr_repositories: List ECR repositories with URIs and creation dates\n- describe_ecr_repository: Get images and details for a specific ECR repository\n- list_eks_clusters: List EKS Kubernetes clusters with status and details\n- describe_eks_cluster: Get detailed EKS cluster configuration\n\nSTORAGE:\n- list_s3_buckets: List S3 buckets with creation dates and regions\n- describe_s3_bucket: Get details about a specific S3 bucket (size, objects, etc.)\n- list_ebs_volumes: List EBS volumes and their attachments, size, type\n- describe_ebs_volume: Get detailed info about a specific EBS volume\n- list_efs_filesystems: List EFS file systems with performance modes\n- describe_efs_filesystem: Get detailed EFS configuration and mount targets\n\nDATABASE:\n- list_rds_instances: List RDS database instances with status and config\n- describe_rds_instance: Get detailed info about a specific RDS instance\n- list_rds_clusters: List RDS Aurora clusters with engine and status\n- list_dynamodb_tables: List DynamoDB tables\n- describe_dynamodb_table: Get detailed DynamoDB table schema and settings\n\nNETWORKING:\n- list_vpcs: List VPCs and their CIDR blocks\n- list_subnets: List subnets across VPCs\n- list_security_groups: List security groups and their rules\n- describe_load_balancers: List and describe load balancers (ALB/NLB/CLB)\n- list_route_tables: List route tables and their routes\n\nMESSAGE QUEUING & EVENTS:\n- list_sqs_queues: List SQS queues with URLs and attributes\n- describe_sqs_queue: Get detailed SQS queue configuration and metrics\n- list_sns_topics: List SNS topics and their ARNs\n- describe_sns_topic: Get SNS topic configuration and subscriptions\n- list_eventbridge_rules: List EventBridge rules with schedules and targets\n- list_eventbridge_buses: List custom EventBridge event buses\n\nMONITORING & LOGS:\n- get_recent_logs: Get recent CloudWatch logs and errors\n- list_cloudwatch_alarms: List CloudWatch alarms and their status\n- describe_cloudwatch_metrics: Get CloudWatch metrics for resources\n- list_log_groups: List CloudWatch log groups\n\nSECURITY & IAM:\n- list_iam_roles: List IAM roles (names only, no sensitive data)\n- list_iam_users: List IAM users (names only, no sensitive data)\n- describe_security_groups: Get security group rules and associations\n- list_kms_keys: List KMS encryption keys\n- describe_kms_key: Get KMS key details and policies\n- list_acm_certificates: List SSL/TLS certificates with status\n- describe_acm_certificate: Get certificate details and validation\n- list_waf_webacls: List WAF Web ACLs for regional and CloudFront\n\nDEVOPS & CI/CD:\n- list_codebuild_projects: List CodeBuild projects with build history\n- list_codepipelines: List CodePipeline pipelines with status\n- describe_codepipeline: Get detailed pipeline configuration and stages\n- list_codecommit_repositories: List CodeCommit Git repositories\n\nANALYTICS & BIG DATA:\n- list_kinesis_streams: List Kinesis data streams\n- describe_kinesis_stream: Get Kinesis stream shards and throughput\n- list_glue_jobs: List AWS Glue ETL jobs and schedules\n- list_glue_databases: List Glue Data Catalog databases\n- list_emr_clusters: List EMR big data clusters\n\nMACHINE LEARNING:\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n\nCACHING:\n- list_elasticache_clusters: List ElastiCache Redis/Memcached clusters\n- describe_elasticache_cluster: Get cache cluster configuration and nodes\n\nAPPLICATION INTEGRATION:\n- list_step_functions: List Step Functions state machines\n- describe_step_function: Get Step Function workflow definition\n\nCOST & BILLING:\n- get_cost_and_usage: Get cost information and usage metrics\n- list_budgets: List AWS Budgets and spending alerts\n\nAI/ML SERVICES:\n- list_bedrock_foundation_models: List available Bedrock foundation models\n- list_bedrock_custom_models: List custom Bedrock models\n- list_bedrock_agents: List Bedrock agents\n- list_bedrock_knowledge_bases: List Bedrock knowledge bases\n- list_bedrock_guardrails: List Bedrock guardrails\n- list_sagemaker_endpoints: List SageMaker model endpoints\n- list_sagemaker_models: List trained SageMaker models\n- list_sagemaker_notebook_instances: List SageMaker notebook instances\n- list_comprehend_jobs: List Comprehend analysis jobs\n- list_textract_jobs: List Textract document analysis jobs\n- list_rekognition_collections: List Rekognition face collections\n\nOTHER SERVICES:\n- list_api_gateways: List API Gateway REST and HTTP APIs\n- list_cloudfront_distributions: List CloudFront distributions\n- list_route53_zones: List Route53 hosted zones\n- list_secrets: List AWS Secrets Manager secrets (names only)\n- list_ssm_parameters: List Systems Manager parameters (names only)\n\nRespond with ONLY a JSON object in this format:\n{\n \"operations\": [\n {\n \"operation\": \"operation_name\",\n \"reason\": \"why this operation is needed\",\n \"parameters\": {\"key\": \"value\"}\n }\n ],\n \"analysis\": \"brief explanation of what the user wants to know\"\n}\n\nIf no AWS operations are needed, return: {\"operations\": [], \"analysis\": \"explanation\"}`, question)\n}\n"}}, "internal/ai/client.go": {"4d7af4d3f13f7651fb538328b6d57eacdfc4f8d7": {"t": 1785746056.599875, "n": 101752, "h": "cf026a9817e78156", "c": "package ai\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\tdefaultAIRetryMaxAttempts = 5\n\tdefaultAIRetryBaseDelay = 1 * time.Second\n\tdefaultAIRetryMaxDelay = 16 * time.Second\n\tdefaultAIHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n\tdefaultClankerCloudLLMURL = \"https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\"\n\tdefaultClankerCloudModel = \"gemini-3.5-flash\"\n)\n\nvar (\n\taiRetryMaxAttempts = envInt(\"CLANKER_AI_RETRY_MAX_ATTEMPTS\", defaultAIRetryMaxAttempts, 1, 8)\n\taiRetryBaseDelay = envDuration(\"CLANKER_AI_RETRY_BASE_DELAY_MS\", defaultAIRetryBaseDelay, 100*time.Millisecond, 30*time.Second)\n\taiRetryMaxDelay = envDuration(\"CLANKER_AI_RETRY_MAX_DELAY_MS\", defaultAIRetryMaxDelay, 250*time.Millisecond, 60*time.Second)\n\taiHTTPClientTimeout = envDuration(\"CLANKER_AI_HTTP_TIMEOUT_MS\", defaultAIHTTPClientTimeout, 5*time.Second, 10*time.Minute)\n)\n\nfunc envInt(key string, fallback, min, max int) int {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc envDuration(key string, fallback, min, max time.Duration) time.Duration {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tmillis, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tvalue := time.Duration(millis) * time.Millisecond\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc normalizeClankerCloudLLMBaseURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\ttrimmed = defaultClankerCloudLLMURL\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tpath := strings.TrimRight(parsed.Path, \"/\")\n\tswitch path {\n\tcase \"\", \"/\":\n\t\tparsed.Path = \"/v1/llm\"\n\tcase \"/v1\":\n\t\tparsed.Path = \"/v1/llm\"\n\tdefault:\n\t\tparsed.Path = path\n\t}\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\tStream bool `json:\"stream,omitempty\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc resolveClankerCloudLLMToken(apiKey string) string {\n\tif key := resolveEnvVarKeyPointer(apiKey); strings.TrimSpace(key) != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key\")); key != \"\" {\n\t\treturn resolveEnvVarKeyPointer(key)\n\t}\n\tif envName := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key_env\")); envName != \"\" {\n\t\tif key := strings.TrimSpace(os.Getenv(envName)); key != \"\" {\n\t\t\treturn key\n\t\t}\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_AUTH_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\treturn \"\"\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"clanker-cloud\":\n\t\tclient.apiKey = resolveClankerCloudLLMToken(client.apiKey)\n\t\tclient.baseURL = normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t\t))\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\temitProgressTrace(\"analysis\", \"Analyzing the request and selecting the live data to gather.\")\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"clanker-cloud\":\n\t\tanalysisResponse, err = c.askClankerCloud(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Gathering live infrastructure context for the selected provider.\")\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Condensing gathered context before the final model call.\")\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Sending the final request to %s.\", c.provider))\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc firstNonEmptyString(values ...string) string {\n\tfor _, value := range values {\n\t\tif strings.TrimSpace(value) != \"\" {\n\t\t\treturn strings.TrimSpace(value)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", profileLLMCall.Model))\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", request.Model))\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) clankerCloudModel() string {\n\tif model := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_MODEL\")); model != \"\" {\n\t\treturn model\n\t}\n\tif model := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.model\")); model != \"\" {\n\t\treturn model\n\t}\n\treturn defaultClankerCloudModel\n}\n\nfunc (c *Client) askClankerCloud(ctx context.Context, prompt string) (string, error) {\n\treturn c.askClankerCloudMessages(ctx, []Message{{\n\t\tRole: \"user\",\n\t\tContent: sanitizeASCII(prompt),\n\t}})\n}\n\nfunc (c *Client) askClankerCloudMessages(ctx context.Context, messages []Message) (string, error) {\n\ttoken := resolveClankerCloudLLMToken(c.apiKey)\n\tif strings.TrimSpace(token) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Clanker Cloud auth token not configured\")\n\t}\n\tbaseURL := normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\tc.baseURL,\n\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t))\n\tmodel := c.clankerCloudModel()\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tStream: true,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Clanker Cloud LLM with model %s.\", model))\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Accept\", \"text/event-stream\")\n\t\thttpReq.Header.Set(\"X-API-Key\", strings.TrimSpace(token))\n\t\tif strings.EqualFold(strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_CLIENT\")), \"desktop-app\") {\n\t\t\thttpReq.Header.Set(\"X-Clanker-Cloud-Client\", \"desktop-app\")\n\t\t}\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tif strings.Contains(strings.ToLower(resp.Header.Get(\"Content-Type\")), \"text/event-stream\") {\n\t\t\t\treply, streamErr := readOpenAICompatibleStreamText(resp.Body)\n\t\t\t\tresp.Body.Close()\n\t\t\t\tif streamErr != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to read Clanker Cloud stream: %w\", streamErr)\n\t\t\t\t}\n\t\t\t\treturn reply, nil\n\t\t\t}\n\t\t\tbody, err = io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Clanker Cloud LLM request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Clanker Cloud LLM\")\n\t}\n\treturn response.Choices[0].Message.Content, nil\n}\n\ntype openAICompatibleStreamChunk struct {\n\tChoices []struct {\n\t\tDelta Message `json:\"delta\"`\n\t\tMessage Message `json:\"message\"`\n\t\tFinishReason string `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tError *struct {\n\t\tMessage string `json:\"message\"`\n\t\tType string `json:\"type\"`\n\t} `json:\"error,omitempty\"`\n}\n\nfunc readOpenAICompatibleStreamText(r io.Reader) (string, error) {\n\tscanner := bufio.NewScanner(r)\n\tscanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)\n\n\tvar sb strings.Builder\n\teventLines := make([]string, 0, 4)\n\tflushEvent := func() error {\n\t\tif len(eventLines) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tdata := strings.TrimSpace(strings.Join(eventLines, \"\\n\"))\n\t\teventLines = eventLines[:0]\n\t\tif data == \"\" || data == \"[DONE]\" {\n\t\t\treturn nil\n\t\t}\n\t\tvar chunk openAICompatibleStreamChunk\n\t\tif err := json.Unmarshal([]byte(data), &chunk); err != nil {\n\t\t\treturn fmt.Errorf(\"decode stream chunk: %w\", err)\n\t\t}\n\t\tif chunk.Error != nil && strings.TrimSpace(chunk.Error.Message) != \"\" {\n\t\t\treturn fmt.Errorf(\"%s\", strings.TrimSpace(chunk.Error.Message))\n\t\t}\n\t\tfor _, choice := range chunk.Choices {\n\t\t\tif choice.Delta.Content != \"\" {\n\t\t\t\tsb.WriteString(choice.Delta.Content)\n\t\t\t} else if choice.Message.Content != \"\" {\n\t\t\t\tsb.WriteString(choice.Message.Content)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\tif err := flushEvent(); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(strings.TrimSpace(line), \":\") {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(line, \"data:\") {\n\t\t\teventLines = append(eventLines, strings.TrimSpace(strings.TrimPrefix(line, \"data:\")))\n\t\t}\n\t}\n\tif err := flushEvent(); err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"read stream: %w\", err)\n\t}\n\tif sb.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"stream returned no assistant reply\")\n\t}\n\treturn sb.String(), nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloudWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\nfunc (c *Client) askClankerCloudWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: conv.SystemPrompt})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\treturn c.askClankerCloudMessages(ctx, messages)\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", model))\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", model))\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "6f7f228edaea19f93aa47e9af23451fb8517b093": {"t": 1785748530.3726342, "n": 101752, "h": "cf026a9817e78156", "c": "package ai\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\tdefaultAIRetryMaxAttempts = 5\n\tdefaultAIRetryBaseDelay = 1 * time.Second\n\tdefaultAIRetryMaxDelay = 16 * time.Second\n\tdefaultAIHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n\tdefaultClankerCloudLLMURL = \"https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\"\n\tdefaultClankerCloudModel = \"gemini-3.5-flash\"\n)\n\nvar (\n\taiRetryMaxAttempts = envInt(\"CLANKER_AI_RETRY_MAX_ATTEMPTS\", defaultAIRetryMaxAttempts, 1, 8)\n\taiRetryBaseDelay = envDuration(\"CLANKER_AI_RETRY_BASE_DELAY_MS\", defaultAIRetryBaseDelay, 100*time.Millisecond, 30*time.Second)\n\taiRetryMaxDelay = envDuration(\"CLANKER_AI_RETRY_MAX_DELAY_MS\", defaultAIRetryMaxDelay, 250*time.Millisecond, 60*time.Second)\n\taiHTTPClientTimeout = envDuration(\"CLANKER_AI_HTTP_TIMEOUT_MS\", defaultAIHTTPClientTimeout, 5*time.Second, 10*time.Minute)\n)\n\nfunc envInt(key string, fallback, min, max int) int {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc envDuration(key string, fallback, min, max time.Duration) time.Duration {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tmillis, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tvalue := time.Duration(millis) * time.Millisecond\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc normalizeClankerCloudLLMBaseURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\ttrimmed = defaultClankerCloudLLMURL\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tpath := strings.TrimRight(parsed.Path, \"/\")\n\tswitch path {\n\tcase \"\", \"/\":\n\t\tparsed.Path = \"/v1/llm\"\n\tcase \"/v1\":\n\t\tparsed.Path = \"/v1/llm\"\n\tdefault:\n\t\tparsed.Path = path\n\t}\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\tStream bool `json:\"stream,omitempty\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc resolveClankerCloudLLMToken(apiKey string) string {\n\tif key := resolveEnvVarKeyPointer(apiKey); strings.TrimSpace(key) != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key\")); key != \"\" {\n\t\treturn resolveEnvVarKeyPointer(key)\n\t}\n\tif envName := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key_env\")); envName != \"\" {\n\t\tif key := strings.TrimSpace(os.Getenv(envName)); key != \"\" {\n\t\t\treturn key\n\t\t}\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_AUTH_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\treturn \"\"\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"clanker-cloud\":\n\t\tclient.apiKey = resolveClankerCloudLLMToken(client.apiKey)\n\t\tclient.baseURL = normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t\t))\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\temitProgressTrace(\"analysis\", \"Analyzing the request and selecting the live data to gather.\")\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"clanker-cloud\":\n\t\tanalysisResponse, err = c.askClankerCloud(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Gathering live infrastructure context for the selected provider.\")\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Condensing gathered context before the final model call.\")\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Sending the final request to %s.\", c.provider))\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc firstNonEmptyString(values ...string) string {\n\tfor _, value := range values {\n\t\tif strings.TrimSpace(value) != \"\" {\n\t\t\treturn strings.TrimSpace(value)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", profileLLMCall.Model))\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", request.Model))\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) clankerCloudModel() string {\n\tif model := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_MODEL\")); model != \"\" {\n\t\treturn model\n\t}\n\tif model := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.model\")); model != \"\" {\n\t\treturn model\n\t}\n\treturn defaultClankerCloudModel\n}\n\nfunc (c *Client) askClankerCloud(ctx context.Context, prompt string) (string, error) {\n\treturn c.askClankerCloudMessages(ctx, []Message{{\n\t\tRole: \"user\",\n\t\tContent: sanitizeASCII(prompt),\n\t}})\n}\n\nfunc (c *Client) askClankerCloudMessages(ctx context.Context, messages []Message) (string, error) {\n\ttoken := resolveClankerCloudLLMToken(c.apiKey)\n\tif strings.TrimSpace(token) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Clanker Cloud auth token not configured\")\n\t}\n\tbaseURL := normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\tc.baseURL,\n\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t))\n\tmodel := c.clankerCloudModel()\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tStream: true,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Clanker Cloud LLM with model %s.\", model))\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Accept\", \"text/event-stream\")\n\t\thttpReq.Header.Set(\"X-API-Key\", strings.TrimSpace(token))\n\t\tif strings.EqualFold(strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_CLIENT\")), \"desktop-app\") {\n\t\t\thttpReq.Header.Set(\"X-Clanker-Cloud-Client\", \"desktop-app\")\n\t\t}\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tif strings.Contains(strings.ToLower(resp.Header.Get(\"Content-Type\")), \"text/event-stream\") {\n\t\t\t\treply, streamErr := readOpenAICompatibleStreamText(resp.Body)\n\t\t\t\tresp.Body.Close()\n\t\t\t\tif streamErr != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to read Clanker Cloud stream: %w\", streamErr)\n\t\t\t\t}\n\t\t\t\treturn reply, nil\n\t\t\t}\n\t\t\tbody, err = io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Clanker Cloud LLM request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Clanker Cloud LLM\")\n\t}\n\treturn response.Choices[0].Message.Content, nil\n}\n\ntype openAICompatibleStreamChunk struct {\n\tChoices []struct {\n\t\tDelta Message `json:\"delta\"`\n\t\tMessage Message `json:\"message\"`\n\t\tFinishReason string `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tError *struct {\n\t\tMessage string `json:\"message\"`\n\t\tType string `json:\"type\"`\n\t} `json:\"error,omitempty\"`\n}\n\nfunc readOpenAICompatibleStreamText(r io.Reader) (string, error) {\n\tscanner := bufio.NewScanner(r)\n\tscanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)\n\n\tvar sb strings.Builder\n\teventLines := make([]string, 0, 4)\n\tflushEvent := func() error {\n\t\tif len(eventLines) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tdata := strings.TrimSpace(strings.Join(eventLines, \"\\n\"))\n\t\teventLines = eventLines[:0]\n\t\tif data == \"\" || data == \"[DONE]\" {\n\t\t\treturn nil\n\t\t}\n\t\tvar chunk openAICompatibleStreamChunk\n\t\tif err := json.Unmarshal([]byte(data), &chunk); err != nil {\n\t\t\treturn fmt.Errorf(\"decode stream chunk: %w\", err)\n\t\t}\n\t\tif chunk.Error != nil && strings.TrimSpace(chunk.Error.Message) != \"\" {\n\t\t\treturn fmt.Errorf(\"%s\", strings.TrimSpace(chunk.Error.Message))\n\t\t}\n\t\tfor _, choice := range chunk.Choices {\n\t\t\tif choice.Delta.Content != \"\" {\n\t\t\t\tsb.WriteString(choice.Delta.Content)\n\t\t\t} else if choice.Message.Content != \"\" {\n\t\t\t\tsb.WriteString(choice.Message.Content)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\tif err := flushEvent(); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(strings.TrimSpace(line), \":\") {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(line, \"data:\") {\n\t\t\teventLines = append(eventLines, strings.TrimSpace(strings.TrimPrefix(line, \"data:\")))\n\t\t}\n\t}\n\tif err := flushEvent(); err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"read stream: %w\", err)\n\t}\n\tif sb.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"stream returned no assistant reply\")\n\t}\n\treturn sb.String(), nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloudWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\nfunc (c *Client) askClankerCloudWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: conv.SystemPrompt})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\treturn c.askClankerCloudMessages(ctx, messages)\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", model))\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", model))\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "798adc6e058d076d81d7b6003761eaa6ef629770": {"t": 1785748531.9812627, "n": 99238, "h": "dddde6a166e38e60", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\tdefaultAIRetryMaxAttempts = 5\n\tdefaultAIRetryBaseDelay = 1 * time.Second\n\tdefaultAIRetryMaxDelay = 16 * time.Second\n\tdefaultAIHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n\tdefaultClankerCloudLLMURL = \"https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\"\n\tdefaultClankerCloudModel = \"gemini-3.5-flash\"\n)\n\nvar (\n\taiRetryMaxAttempts = envInt(\"CLANKER_AI_RETRY_MAX_ATTEMPTS\", defaultAIRetryMaxAttempts, 1, 8)\n\taiRetryBaseDelay = envDuration(\"CLANKER_AI_RETRY_BASE_DELAY_MS\", defaultAIRetryBaseDelay, 100*time.Millisecond, 30*time.Second)\n\taiRetryMaxDelay = envDuration(\"CLANKER_AI_RETRY_MAX_DELAY_MS\", defaultAIRetryMaxDelay, 250*time.Millisecond, 60*time.Second)\n\taiHTTPClientTimeout = envDuration(\"CLANKER_AI_HTTP_TIMEOUT_MS\", defaultAIHTTPClientTimeout, 5*time.Second, 10*time.Minute)\n)\n\nfunc envInt(key string, fallback, min, max int) int {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc envDuration(key string, fallback, min, max time.Duration) time.Duration {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tmillis, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tvalue := time.Duration(millis) * time.Millisecond\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc normalizeClankerCloudLLMBaseURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\ttrimmed = defaultClankerCloudLLMURL\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tpath := strings.TrimRight(parsed.Path, \"/\")\n\tswitch path {\n\tcase \"\", \"/\":\n\t\tparsed.Path = \"/v1/llm\"\n\tcase \"/v1\":\n\t\tparsed.Path = \"/v1/llm\"\n\tdefault:\n\t\tparsed.Path = path\n\t}\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc resolveClankerCloudLLMToken(apiKey string) string {\n\tif key := resolveEnvVarKeyPointer(apiKey); strings.TrimSpace(key) != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key\")); key != \"\" {\n\t\treturn resolveEnvVarKeyPointer(key)\n\t}\n\tif envName := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key_env\")); envName != \"\" {\n\t\tif key := strings.TrimSpace(os.Getenv(envName)); key != \"\" {\n\t\t\treturn key\n\t\t}\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_AUTH_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\treturn \"\"\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"clanker-cloud\":\n\t\tclient.apiKey = resolveClankerCloudLLMToken(client.apiKey)\n\t\tclient.baseURL = normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t\t))\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\temitProgressTrace(\"analysis\", \"Analyzing the request and selecting the live data to gather.\")\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"clanker-cloud\":\n\t\tanalysisResponse, err = c.askClankerCloud(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Gathering live infrastructure context for the selected provider.\")\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Condensing gathered context before the final model call.\")\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Sending the final request to %s.\", c.provider))\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc firstNonEmptyString(values ...string) string {\n\tfor _, value := range values {\n\t\tif strings.TrimSpace(value) != \"\" {\n\t\t\treturn strings.TrimSpace(value)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", profileLLMCall.Model))\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", request.Model))\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) clankerCloudModel() string {\n\tif model := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_MODEL\")); model != \"\" {\n\t\treturn model\n\t}\n\tif model := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.model\")); model != \"\" {\n\t\treturn model\n\t}\n\treturn defaultClankerCloudModel\n}\n\nfunc (c *Client) askClankerCloud(ctx context.Context, prompt string) (string, error) {\n\treturn c.askClankerCloudMessages(ctx, []Message{{\n\t\tRole: \"user\",\n\t\tContent: sanitizeASCII(prompt),\n\t}})\n}\n\nfunc (c *Client) askClankerCloudMessages(ctx context.Context, messages []Message) (string, error) {\n\ttoken := resolveClankerCloudLLMToken(c.apiKey)\n\tif strings.TrimSpace(token) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Clanker Cloud auth token not configured\")\n\t}\n\tbaseURL := normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\tc.baseURL,\n\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t))\n\tmodel := c.clankerCloudModel()\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Clanker Cloud LLM with model %s.\", model))\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"X-API-Key\", strings.TrimSpace(token))\n\t\tif strings.EqualFold(strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_CLIENT\")), \"desktop-app\") {\n\t\t\thttpReq.Header.Set(\"X-Clanker-Cloud-Client\", \"desktop-app\")\n\t\t}\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Clanker Cloud LLM request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Clanker Cloud LLM\")\n\t}\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloudWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\nfunc (c *Client) askClankerCloudWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: conv.SystemPrompt})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\treturn c.askClankerCloudMessages(ctx, messages)\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", model))\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", model))\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "8246bb67c62106e2b09aa1bfacfdddbef9cb2a2f": {"t": 1785748533.4159107, "n": 99225, "h": "9ad4d141cb9a46eb", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\tdefaultAIRetryMaxAttempts = 5\n\tdefaultAIRetryBaseDelay = 1 * time.Second\n\tdefaultAIRetryMaxDelay = 16 * time.Second\n\tdefaultAIHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n\tdefaultClankerCloudLLMURL = \"https://clanker-auth-gw-zc0ce3o.uk.gateway.dev/v1/llm\"\n\tdefaultClankerCloudModel = \"gemini-3.5-flash\"\n)\n\nvar (\n\taiRetryMaxAttempts = envInt(\"CLANKER_AI_RETRY_MAX_ATTEMPTS\", defaultAIRetryMaxAttempts, 1, 8)\n\taiRetryBaseDelay = envDuration(\"CLANKER_AI_RETRY_BASE_DELAY_MS\", defaultAIRetryBaseDelay, 100*time.Millisecond, 30*time.Second)\n\taiRetryMaxDelay = envDuration(\"CLANKER_AI_RETRY_MAX_DELAY_MS\", defaultAIRetryMaxDelay, 250*time.Millisecond, 60*time.Second)\n\taiHTTPClientTimeout = envDuration(\"CLANKER_AI_HTTP_TIMEOUT_MS\", defaultAIHTTPClientTimeout, 5*time.Second, 10*time.Minute)\n)\n\nfunc envInt(key string, fallback, min, max int) int {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc envDuration(key string, fallback, min, max time.Duration) time.Duration {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tmillis, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tvalue := time.Duration(millis) * time.Millisecond\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc normalizeClankerCloudLLMBaseURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\ttrimmed = defaultClankerCloudLLMURL\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tpath := strings.TrimRight(parsed.Path, \"/\")\n\tswitch path {\n\tcase \"\", \"/\":\n\t\tparsed.Path = \"/v1/llm\"\n\tcase \"/v1\":\n\t\tparsed.Path = \"/v1/llm\"\n\tdefault:\n\t\tparsed.Path = path\n\t}\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc resolveClankerCloudLLMToken(apiKey string) string {\n\tif key := resolveEnvVarKeyPointer(apiKey); strings.TrimSpace(key) != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key\")); key != \"\" {\n\t\treturn resolveEnvVarKeyPointer(key)\n\t}\n\tif envName := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.api_key_env\")); envName != \"\" {\n\t\tif key := strings.TrimSpace(os.Getenv(envName)); key != \"\" {\n\t\t\treturn key\n\t\t}\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_AUTH_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\tif key := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_TOKEN\")); key != \"\" {\n\t\treturn key\n\t}\n\treturn \"\"\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"clanker-cloud\":\n\t\tclient.apiKey = resolveClankerCloudLLMToken(client.apiKey)\n\t\tclient.baseURL = normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t\t))\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\temitProgressTrace(\"analysis\", \"Analyzing the request and selecting the live data to gather.\")\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"clanker-cloud\":\n\t\tanalysisResponse, err = c.askClankerCloud(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Gathering live infrastructure context for the selected provider.\")\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Condensing gathered context before the final model call.\")\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Sending the final request to %s.\", c.provider))\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc firstNonEmptyString(values ...string) string {\n\tfor _, value := range values {\n\t\tif strings.TrimSpace(value) != \"\" {\n\t\t\treturn strings.TrimSpace(value)\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", profileLLMCall.Model))\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", request.Model))\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) clankerCloudModel() string {\n\tif model := strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_LLM_MODEL\")); model != \"\" {\n\t\treturn model\n\t}\n\tif model := strings.TrimSpace(viper.GetString(\"ai.providers.clanker-cloud.model\")); model != \"\" {\n\t\treturn model\n\t}\n\treturn defaultClankerCloudModel\n}\n\nfunc (c *Client) askClankerCloud(ctx context.Context, prompt string) (string, error) {\n\treturn c.askClankerCloudMessages(ctx, []Message{{\n\t\tRole: \"user\",\n\t\tContent: sanitizeASCII(prompt),\n\t}})\n}\n\nfunc (c *Client) askClankerCloudMessages(ctx context.Context, messages []Message) (string, error) {\n\ttoken := resolveClankerCloudLLMToken(c.apiKey)\n\tif strings.TrimSpace(token) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Clanker Cloud auth token not configured\")\n\t}\n\tbaseURL := normalizeClankerCloudLLMBaseURL(firstNonEmptyString(\n\t\tc.baseURL,\n\t\tos.Getenv(\"CLANKER_CLOUD_LLM_BASE_URL\"),\n\t\tviper.GetString(\"ai.providers.clanker-cloud.base_url\"),\n\t))\n\tmodel := c.clankerCloudModel()\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Clanker Cloud LLM with model %s.\", model))\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, token)\n\t\tif strings.EqualFold(strings.TrimSpace(os.Getenv(\"CLANKER_CLOUD_CLIENT\")), \"desktop-app\") {\n\t\t\thttpReq.Header.Set(\"X-Clanker-Cloud-Client\", \"desktop-app\")\n\t\t}\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Clanker Cloud LLM request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Clanker Cloud LLM\")\n\t}\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloudWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\nfunc (c *Client) askClankerCloudWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: conv.SystemPrompt})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\treturn c.askClankerCloudMessages(ctx, messages)\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", model))\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", model))\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"clanker-cloud\":\n\t\tresponse, err = c.askClankerCloud(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"clanker-cloud\":\n\t\treturn c.askClankerCloud(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "38ea0ea3b1abd71e666c7e9be209a0d18021cbda": {"t": 1785748534.8764932, "n": 93380, "h": "c3430273c9c70abf", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\tdefaultAIRetryMaxAttempts = 5\n\tdefaultAIRetryBaseDelay = 1 * time.Second\n\tdefaultAIRetryMaxDelay = 16 * time.Second\n\tdefaultAIHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nvar (\n\taiRetryMaxAttempts = envInt(\"CLANKER_AI_RETRY_MAX_ATTEMPTS\", defaultAIRetryMaxAttempts, 1, 8)\n\taiRetryBaseDelay = envDuration(\"CLANKER_AI_RETRY_BASE_DELAY_MS\", defaultAIRetryBaseDelay, 100*time.Millisecond, 30*time.Second)\n\taiRetryMaxDelay = envDuration(\"CLANKER_AI_RETRY_MAX_DELAY_MS\", defaultAIRetryMaxDelay, 250*time.Millisecond, 60*time.Second)\n\taiHTTPClientTimeout = envDuration(\"CLANKER_AI_HTTP_TIMEOUT_MS\", defaultAIHTTPClientTimeout, 5*time.Second, 10*time.Minute)\n)\n\nfunc envInt(key string, fallback, min, max int) int {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc envDuration(key string, fallback, min, max time.Duration) time.Duration {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback\n\t}\n\tmillis, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\tvalue := time.Duration(millis) * time.Millisecond\n\tif value < min {\n\t\treturn min\n\t}\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\temitProgressTrace(\"analysis\", \"Analyzing the request and selecting the live data to gather.\")\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Gathering live infrastructure context for the selected provider.\")\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\temitProgressTrace(\"context\", \"Condensing gathered context before the final model call.\")\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Sending the final request to %s.\", c.provider))\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", profileLLMCall.Model))\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", request.Model))\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling AWS Bedrock with model %s.\", profileLLMCall.Model))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Anthropic with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling MiniMax with model %s.\", model))\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\temitProgressTrace(\"provider\", \"Calling OpenAI through the saved ChatGPT OAuth session.\")\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling OpenAI-compatible chat completions with model %s.\", model))\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling GitHub Models with model %s.\", model))\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Gemini with model %s.\", model))\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\temitProgressTrace(\"provider\", fmt.Sprintf(\"Calling Cohere with model %s.\", model))\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "90d9139a81fcfaf63ff4a870565d5cae61db83f8": {"t": 1785748536.4562066, "n": 90330, "h": "5cbd3ddfec538cae", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc shouldUseOpenAIOAuth(apiKey, baseURL string) bool {\n\tif !IsOpenAIOAuthActive() {\n\t\treturn false\n\t}\n\tif strings.TrimSpace(os.Getenv(\"OPENAI_OAUTH_TOKEN\")) != \"\" {\n\t\treturn true\n\t}\n\tif isLocalModelInferenceEndpoint(baseURL) {\n\t\treturn false\n\t}\n\treturn strings.TrimSpace(apiKey) == \"\"\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif shouldUseOpenAIOAuth(c.apiKey, c.baseURL) {\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "b6a445505faaf64ad7840a87b42b7ca254e42d3d": {"t": 1785748537.8751183, "n": 90232, "h": "6ee0ce1d8fd071c9", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// If no API key is configured but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// If no API key but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "2aa4d347ad28f3829407d405ddbcde53f51f2702": {"t": 1785748539.9246118, "n": 90278, "h": "224641871203c7b4", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\t// ChatTemplateKwargs is a vLLM/SGLang vendor extension. The most useful\n\t// case is `enable_thinking: false` for Qwen3-style reasoning models —\n\t// it skips the internal \"thinking\" trace and emits the answer directly,\n\t// dropping plan-generation latency from ~60-90s to ~1-10s. Empty map\n\t// omits the field entirely thanks to omitempty, so the request shape\n\t// is unchanged for non-reasoning providers.\n\tChatTemplateKwargs map[string]interface{} `json:\"chat_template_kwargs,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// If no API key is configured but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\trequest.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// If no API key but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tif v := viper.GetStringMap(\"ai.providers.openai.chat_template_kwargs\"); len(v) > 0 {\n\t\treqBody.ChatTemplateKwargs = v\n\t}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "b1ebabc88fa8f0c7dd5050f8012214d104f5d3ca": {"t": 1785748542.3712754, "n": 89253, "h": "19cd15bbdcd38887", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// If no API key is configured but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.AskCodex(ctx, prompt)\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// If no API key but OAuth is available, use the Codex Responses API.\n\tif strings.TrimSpace(c.apiKey) == \"\" && IsOpenAIOAuthActive() {\n\t\treturn c.askCodexWithHistory(ctx, conv)\n\t}\n\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured (or run 'clanker auth login' for OAuth)\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "e41a40bad5024b1f410caeee5ca0b749a5ad3d86": {"t": 1785748543.9488146, "n": 88804, "h": "730e4898f3ae53bd", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\ntype contextPromptBudget struct {\n\tmaxChars int\n\tchunkChars int\n\tmaxChunks int\n\tfallbackChars int\n\tchunkFallbackChars int\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n\tdefaultOpenAIBaseURL = \"https://api.openai.com/v1\"\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc normalizeLocalModelInferenceURL(raw string) string {\n\ttrimmed := strings.TrimSpace(raw)\n\tif trimmed == \"\" {\n\t\treturn \"\"\n\t}\n\n\tparsed, err := url.Parse(trimmed)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn strings.TrimRight(trimmed, \"/\")\n\t}\n\n\tif parsed.Path == \"\" || parsed.Path == \"/\" {\n\t\tparsed.Path = \"/v1\"\n\t}\n\n\treturn strings.TrimRight(parsed.String(), \"/\")\n}\n\nfunc isLocalModelInferenceEndpoint(raw string) bool {\n\tparsed, err := url.Parse(strings.TrimSpace(raw))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\thost := strings.TrimSpace(parsed.Hostname())\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif strings.EqualFold(host, \"localhost\") {\n\t\treturn true\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\treturn ip.IsLoopback() || ip.IsUnspecified()\n}\n\nfunc applyModelProviderAuthHeader(req *http.Request, apiKey string) {\n\tif req == nil {\n\t\treturn\n\t}\n\tif key := strings.TrimSpace(apiKey); key != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+key)\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = defaultOpenAIBaseURL\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = defaultOpenAIBaseURL\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) resolveLocalModelInferenceURL(profile *awsclient.AIProfile) string {\n\tif profile != nil && strings.TrimSpace(profile.LocalModelInferenceURL) != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(profile.LocalModelInferenceURL)\n\t}\n\tif endpoint := strings.TrimSpace(viper.GetString(\"ai.providers.openai.local_model_inference_url\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\tif endpoint := strings.TrimSpace(os.Getenv(\"LOCAL_MODEL_INFERENCE_URL\")); endpoint != \"\" {\n\t\treturn normalizeLocalModelInferenceURL(endpoint)\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tendpoint := strings.TrimRight(c.baseURL, \"/\") + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tapplyModelProviderAuthHeader(req, c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", endpoint, bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(req, c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\tc.baseURL = defaultOpenAIBaseURL\n\tif localModelInferenceURL := c.resolveLocalModelInferenceURL(profileLLMCall); localModelInferenceURL != \"\" {\n\t\tc.baseURL = localModelInferenceURL\n\t}\n\tif strings.TrimSpace(c.apiKey) == \"\" && !isLocalModelInferenceEndpoint(c.baseURL) {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tapplyModelProviderAuthHeader(httpReq, c.apiKey)\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tfallbackLimit := c.contextPromptBudget().fallbackChars\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) contextPromptBudget() contextPromptBudget {\n\tbudget := contextPromptBudget{\n\t\tmaxChars: 120000,\n\t\tchunkChars: 120000,\n\t\tmaxChunks: 6,\n\t\tfallbackChars: 80000,\n\t\tchunkFallbackChars: 5000,\n\t}\n\n\tif configured := viper.GetInt(\"ai.max_prompt_chars\"); configured > 0 {\n\t\tbudget.maxChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.chunk_chars\"); configured > 0 {\n\t\tbudget.chunkChars = configured\n\t}\n\tif configured := viper.GetInt(\"ai.max_chunks\"); configured > 0 {\n\t\tbudget.maxChunks = configured\n\t}\n\n\tif c.provider == \"github-models\" {\n\t\tif budget.maxChars > 12000 {\n\t\t\tbudget.maxChars = 12000\n\t\t}\n\t\tif budget.chunkChars > 9000 {\n\t\t\tbudget.chunkChars = 9000\n\t\t}\n\t\tbudget.fallbackChars = budget.maxChars\n\t\tbudget.chunkFallbackChars = 2000\n\n\t\tif strings.Contains(c.githubModelsActiveModel(), \"gpt-5-chat\") {\n\t\t\tif budget.maxChars > 8000 {\n\t\t\t\tbudget.maxChars = 8000\n\t\t\t}\n\t\t\tif budget.chunkChars > 6000 {\n\t\t\t\tbudget.chunkChars = 6000\n\t\t\t}\n\t\t\tbudget.fallbackChars = budget.maxChars\n\t\t\tbudget.chunkFallbackChars = 1500\n\t\t}\n\t}\n\n\tif budget.maxChars <= 0 {\n\t\tbudget.maxChars = 120000\n\t}\n\tif budget.chunkChars <= 0 || budget.chunkChars > budget.maxChars {\n\t\tbudget.chunkChars = budget.maxChars\n\t}\n\tif budget.maxChunks <= 0 {\n\t\tbudget.maxChunks = 6\n\t}\n\tif budget.fallbackChars <= 0 || budget.fallbackChars > budget.maxChars {\n\t\tbudget.fallbackChars = budget.maxChars\n\t}\n\tif budget.chunkFallbackChars <= 0 || budget.chunkFallbackChars > budget.chunkChars {\n\t\tbudget.chunkFallbackChars = budget.chunkChars\n\t}\n\n\treturn budget\n}\n\nfunc (c *Client) githubModelsActiveModel() string {\n\tif c.provider != \"github-models\" {\n\t\treturn \"\"\n\t}\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\treturn \"openai/gpt-5.4\"\n\t}\n\treturn model\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\tbudget := c.contextPromptBudget()\n\n\tif len(contextText) <= budget.maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunks := chunkString(contextText, budget.chunkChars)\n\t// Limit total chunks to keep latency reasonable\n\tif len(chunks) > budget.maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), budget.maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, budget.maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), budget.chunkChars, budget.maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tchunkFallback := budget.chunkFallbackChars\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > budget.maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > budget.maxChars {\n\t\t\t\treturn final[:budget.maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:budget.maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "30a78462c64ac561478ff94d93133d51c6fcef52": {"t": 1785748546.0330925, "n": 84788, "h": "f569feac59220bed", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"minimax\":\n\t\tanalysisResponse, err = c.askMiniMax(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMax sends a prompt to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMax(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(prompt)}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMaxWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askMiniMaxWithHistory sends a multi-turn request to MiniMax using the Anthropic-compatible API.\nfunc (c *Client) askMiniMaxWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"MiniMax API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"MiniMax-M2.5\"\n\t}\n\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v1/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"MiniMax API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from MiniMax\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"minimax\":\n\t\tresponse, err = c.askMiniMax(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"minimax\":\n\t\treturn c.askMiniMax(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "c50ad2b9c2b159198f4c076adf75c5d0a6ea765b": {"t": 1785748549.0996475, "n": 78563, "h": "2d5005fce4b0d38b", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n\taiHTTPClientTimeout = 120 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{Timeout: aiHTTPClientTimeout}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "b033083f56b59465b0c5711de178f071c527e100": {"t": 1785748551.9955986, "n": 78267, "h": "f208539f5a43932d", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"github-models\":\n\t\tclient.baseURL = \"https://models.github.ai\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"github-models\":\n\t\tanalysisResponse, err = c.askGitHubModels(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) resolveGitHubModelsToken(ctx context.Context) string {\n\tif strings.TrimSpace(c.apiKey) != \"\" {\n\t\treturn strings.TrimSpace(c.apiKey)\n\t}\n\tcmd := exec.CommandContext(ctx, \"gh\", \"auth\", \"token\")\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(string(output))\n}\n\nfunc (c *Client) askGitHubModels(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: []Message{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send GitHub Models request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read GitHub Models response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal GitHub Models response: %w\", err)\n\t}\n\tif len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from GitHub Models\")\n\t}\n\treturn parsed.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModelsWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\nfunc (c *Client) askGitHubModelsWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: sanitizeASCII(conv.SystemPrompt)})\n\t}\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"openai/gpt-5.4\"\n\t}\n\n\ttoken := c.resolveGitHubModelsToken(ctx)\n\tif token == \"\" {\n\t\treturn \"\", fmt.Errorf(\"GitHub auth token not configured; run 'gh auth login' or provide a token with models access\")\n\t}\n\n\treqBody := OpenAIRequest{Model: model, Messages: messages}\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/inference/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\t\thttpReq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\thttpReq.Header.Set(\"X-GitHub-Api-Version\", \"2026-03-10\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"GitHub Models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"no response content from GitHub Models\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"github-models\":\n\t\tresponse, err = c.askGitHubModels(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"github-models\":\n\t\treturn c.askGitHubModels(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "350625344ff2b7a59f71acad6a6650acc9bb40c8": {"t": 1785748553.3904579, "n": 71826, "h": "9aa44e396a006eef", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype cohereChatMessage struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype cohereChatRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []cohereChatMessage `json:\"messages\"`\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n}\n\ntype cohereChatResponse struct {\n\tMessage struct {\n\t\tContent []struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"content\"`\n\t} `json:\"message\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc newBedrockInvokeCommand(ctx context.Context, model, bodyFilePath, awsProfile, region, responsePath string) *exec.Cmd {\n\targs := []string{\n\t\t\"bedrock-runtime\",\n\t\t\"invoke-model\",\n\t\t\"--model-id\", model,\n\t\t\"--body\", \"fileb://\" + bodyFilePath,\n\t}\n\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\targs = append(args, \"--profile\", trimmedProfile)\n\t}\n\tif trimmedRegion := strings.TrimSpace(region); trimmedRegion != \"\" {\n\t\targs = append(args, \"--region\", trimmedRegion)\n\t}\n\n\targs = append(args, responsePath)\n\tcmd := exec.CommandContext(ctx, \"aws\", args...)\n\tif trimmedProfile := strings.TrimSpace(awsProfile); trimmedProfile != \"\" {\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", trimmedProfile))\n\t}\n\treturn cmd\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"cohere\":\n\t\tclient.baseURL = \"https://api.cohere.com\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"cohere\":\n\t\tanalysisResponse, err = c.askCohere(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askCohere(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: []cohereChatMessage{{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(prompt),\n\t\t}},\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohereWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = newBedrockInvokeCommand(ctx, profileLLMCall.Model, bodyFilePath, profileLLMCall.AWSProfile, profileLLMCall.Region, tmpFilePath)\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\nfunc (c *Client) askCohereWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Cohere API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"command-a-03-2025\"\n\t}\n\n\tmessages := make([]cohereChatMessage, 0, len(conv.Messages)+1)\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: \"system\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\trole := strings.TrimSpace(m.Role)\n\t\tif role == \"\" {\n\t\t\trole = \"user\"\n\t\t}\n\t\tmessages = append(messages, cohereChatMessage{\n\t\t\tRole: role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\treqBody := cohereChatRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/v2/chat\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Cohere API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed cohereChatResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\ttext := extractCohereText(parsed.Message.Content)\n\tif text == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response content from Cohere\")\n\t}\n\n\treturn text, nil\n}\n\nfunc extractCohereText(parts []struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text\"`\n}) string {\n\tvar builder strings.Builder\n\tfor _, part := range parts {\n\t\tif strings.TrimSpace(part.Text) == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(part.Text)\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"cohere\":\n\t\tresponse, err = c.askCohere(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"cohere\":\n\t\treturn c.askCohere(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "74e2c10bec57052b47db5d9145b57e12cc43edd0": {"t": 1785748554.839068, "n": 65238, "h": "d01aa7105ab04388", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\n// ConversationContext maintains state across multiple LLM turns for agentic workflows\ntype ConversationContext struct {\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n\tMessages []Message `json:\"messages\"`\n\tMaxHistory int `json:\"-\"`\n}\n\n// NewConversationContext creates a new conversation context with a system prompt\nfunc NewConversationContext(systemPrompt string) *ConversationContext {\n\treturn &ConversationContext{\n\t\tSystemPrompt: systemPrompt,\n\t\tMessages: make([]Message, 0),\n\t\tMaxHistory: 20,\n\t}\n}\n\n// AddUserMessage adds a user message to the conversation\nfunc (c *ConversationContext) AddUserMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"user\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// AddAssistantMessage adds an assistant response to the conversation\nfunc (c *ConversationContext) AddAssistantMessage(content string) {\n\tc.Messages = append(c.Messages, Message{\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t})\n\tc.trimHistory()\n}\n\n// trimHistory keeps only the most recent messages\nfunc (c *ConversationContext) trimHistory() {\n\tif len(c.Messages) > c.MaxHistory {\n\t\t// Keep first message (usually important context) and last N-1 messages\n\t\tc.Messages = append(\n\t\t\tc.Messages[:1],\n\t\t\tc.Messages[len(c.Messages)-(c.MaxHistory-1):]...,\n\t\t)\n\t}\n}\n\n// GetMessages returns all messages for API call\nfunc (c *ConversationContext) GetMessages() []Message {\n\treturn c.Messages\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\t\"--region\", profileLLMCall.Region,\n\t\t\ttmpFilePath)\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// AskWithContext sends a prompt maintaining conversation context for multi-turn interactions.\n// This is used by agentic workflows that need to maintain state across multiple LLM calls.\nfunc (c *Client) AskWithContext(ctx context.Context, conv *ConversationContext, prompt string) (string, error) {\n\t// Add user message to history\n\tconv.AddUserMessage(prompt)\n\n\tvar response string\n\tvar err error\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropicWithHistory(ctx, conv)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAIWithHistory(ctx, conv)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGeminiWithHistory(ctx, conv)\n\tdefault:\n\t\tresponse, err = c.askBedrockWithHistory(ctx, conv)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add assistant response to history\n\tconv.AddAssistantMessage(response)\n\n\treturn response, nil\n}\n\n// askBedrockWithHistory sends a multi-turn request to Bedrock\nfunc (c *Client) askBedrockWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"user\",\n\t\t\tContent: sanitizeASCII(conv.SystemPrompt),\n\t\t})\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: \"Understood. I will follow these instructions.\",\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: m.Role,\n\t\t\tContent: sanitizeASCII(m.Content),\n\t\t})\n\t}\n\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: messages,\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-conv-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-conv-resp-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\t\"--region\", profileLLMCall.Region,\n\t\t\ttmpFilePath)\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\t}\n\n\trespData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar claudeResp ClaudeResponse\n\tif err := json.Unmarshal(respData, &claudeResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, content := range claudeResp.Content {\n\t\tif strings.TrimSpace(content.Text) != \"\" {\n\t\t\treturn content.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Bedrock\")\n}\n\n// askAnthropicWithHistory sends a multi-turn request to Anthropic API\nfunc (c *Client) askAnthropicWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Build messages array from conversation context\n\tmessages := make([]anthropicMessage, 0, len(conv.Messages)+1)\n\n\t// Add system prompt as first user message if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"user\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(conv.SystemPrompt)}},\n\t\t})\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: \"assistant\",\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": \"Understood. I will follow these instructions.\"}},\n\t\t})\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tmessages = append(messages, anthropicMessage{\n\t\t\tRole: m.Role,\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": sanitizeASCII(m.Content)}},\n\t\t})\n\t}\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\n// askOpenAIWithHistory sends a multi-turn request to OpenAI API\nfunc (c *Client) askOpenAIWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\t// Build messages array\n\tmessages := make([]Message, 0, len(conv.Messages)+1)\n\n\t// Add system prompt if present\n\tif conv.SystemPrompt != \"\" {\n\t\tmessages = append(messages, Message{\n\t\t\tRole: \"system\",\n\t\t\tContent: conv.SystemPrompt,\n\t\t})\n\t}\n\n\tmessages = append(messages, conv.Messages...)\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gpt-4\"\n\t}\n\n\treqBody := OpenAIRequest{\n\t\tModel: model,\n\t\tMessages: messages,\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+strings.TrimSpace(c.apiKey))\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"OpenAI API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed OpenAIResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(parsed.Choices) > 0 && strings.TrimSpace(parsed.Choices[0].Message.Content) != \"\" {\n\t\treturn parsed.Choices[0].Message.Content, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from OpenAI\")\n}\n\n// askGeminiWithHistory sends a multi-turn request to Gemini\nfunc (c *Client) askGeminiWithHistory(ctx context.Context, conv *ConversationContext) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"Gemini client not initialized\")\n\t}\n\n\t// Build combined prompt from conversation history\n\tvar promptBuilder strings.Builder\n\tif conv.SystemPrompt != \"\" {\n\t\tpromptBuilder.WriteString(\"System: \")\n\t\tpromptBuilder.WriteString(conv.SystemPrompt)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tfor _, m := range conv.Messages {\n\t\tif m.Role == \"user\" {\n\t\t\tpromptBuilder.WriteString(\"User: \")\n\t\t} else {\n\t\t\tpromptBuilder.WriteString(\"Assistant: \")\n\t\t}\n\t\tpromptBuilder.WriteString(m.Content)\n\t\tpromptBuilder.WriteString(\"\\n\\n\")\n\t}\n\n\tpromptBuilder.WriteString(\"Assistant: \")\n\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile: %w\", err)\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tmodel = \"gemini-2.0-flash\"\n\t}\n\n\tvar result *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresult, err = c.geminiClient.Models.GenerateContent(ctx, model,\n\t\t\tgenai.Text(promptBuilder.String()), nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"Gemini API call failed: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif result == nil || len(result.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar textResult strings.Builder\n\tfor _, part := range result.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\ttextResult.WriteString(part.Text)\n\t\t}\n\t}\n\n\tif textResult.Len() == 0 {\n\t\treturn \"\", fmt.Errorf(\"no text content in Gemini response\")\n\t}\n\n\treturn textResult.String(), nil\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "3cfcea7871e661f3b8cbf07cf90bdfb1da91ab4a": {"t": 1785748556.1703491, "n": 51527, "h": "e384ad8b823451f5", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\nconst (\n\taiRetryMaxAttempts = 5\n\taiRetryBaseDelay = 1 * time.Second\n\taiRetryMaxDelay = 16 * time.Second\n)\n\nfunc aiRetryDelay(retryIndex int) time.Duration {\n\tif retryIndex < 0 {\n\t\tretryIndex = 0\n\t}\n\td := aiRetryBaseDelay << retryIndex\n\tif d > aiRetryMaxDelay {\n\t\treturn aiRetryMaxDelay\n\t}\n\treturn d\n}\n\nfunc waitForAIRetry(ctx context.Context, d time.Duration) error {\n\tt := time.NewTimer(d)\n\tdefer t.Stop()\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-t.C:\n\t\treturn nil\n\t}\n}\n\nfunc isRetryableHTTPStatus(code int) bool {\n\tswitch code {\n\tcase http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isRetryableProviderErrorText(s string) bool {\n\ttext := strings.ToLower(strings.TrimSpace(s))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\tmarkers := []string{\n\t\t\"overloaded\",\n\t\t\"overload\",\n\t\t\"rate limit\",\n\t\t\"rate_limit\",\n\t\t\"throttl\",\n\t\t\"timeout\",\n\t\t\"temporar\",\n\t\t\"try again\",\n\t\t\"unavailable\",\n\t}\n\tfor _, marker := range markers {\n\t\tif strings.Contains(text, marker) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryAfterDelay(h http.Header) (time.Duration, bool) {\n\traw := strings.TrimSpace(h.Get(\"Retry-After\"))\n\tif raw == \"\" {\n\t\treturn 0, false\n\t}\n\tif secs, err := strconv.Atoi(raw); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\td := time.Duration(secs) * time.Second\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\tif ts, err := http.ParseTime(raw); err == nil {\n\t\td := time.Until(ts)\n\t\tif d <= 0 {\n\t\t\treturn 0, false\n\t\t}\n\t\tif d > aiRetryMaxDelay {\n\t\t\td = aiRetryMaxDelay\n\t\t}\n\t\treturn d, true\n\t}\n\treturn 0, false\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\tvar output []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\toutput, err = cmd.CombinedOutput()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(string(output)+\" \"+err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t\tcmd = exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\t\"--region\", profileLLMCall.Region,\n\t\t\ttmpFilePath)\n\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tvar resp *genai.GenerateContentResponse\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, err = c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(err.Error()) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tvar body []byte\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\tresp, doErr := client.Do(req)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t\t}\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\n\t\treq, err = http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif strings.EqualFold(model, \"claude-3-sonnet-20240229\") {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\tmodel = strings.TrimSpace(latest)\n\t\t}\n\t}\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\tclient := &http.Client{}\n\tvar body []byte\n\ttriedModelFallback := false\n\tfor attempt := 1; attempt <= aiRetryMaxAttempts; attempt++ {\n\t\thttpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\t\tif reqErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", reqErr)\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\t\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\t\tresp, doErr := client.Do(httpReq)\n\t\tif doErr != nil {\n\t\t\tif attempt == aiRetryMaxAttempts || !isRetryableProviderErrorText(doErr.Error()) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", doErr)\n\t\t\t}\n\t\t\tif wErr := waitForAIRetry(ctx, aiRetryDelay(attempt-1)); wErr != nil {\n\t\t\t\treturn \"\", wErr\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\n\t\tif !triedModelFallback && anthropicModelNotFound(resp.StatusCode, body) {\n\t\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\t\tif lErr == nil && strings.TrimSpace(latest) != \"\" {\n\t\t\t\treqBody.Model = strings.TrimSpace(latest)\n\t\t\t\tjsonData, err = json.Marshal(reqBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to marshal anthropic fallback request: %w\", err)\n\t\t\t\t}\n\t\t\t\ttriedModelFallback = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif attempt == aiRetryMaxAttempts || !(isRetryableHTTPStatus(resp.StatusCode) || isRetryableProviderErrorText(string(body))) {\n\t\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t\t}\n\n\t\tdelay := aiRetryDelay(attempt - 1)\n\t\tif ra, ok := retryAfterDelay(resp.Header); ok {\n\t\t\tdelay = ra\n\t\t}\n\t\tif wErr := waitForAIRetry(ctx, delay); wErr != nil {\n\t\t\treturn \"\", wErr\n\t\t}\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc anthropicModelNotFound(statusCode int, body []byte) bool {\n\tif statusCode != http.StatusNotFound {\n\t\treturn false\n\t}\n\ttext := strings.ToLower(strings.TrimSpace(string(body)))\n\tif text == \"\" {\n\t\treturn false\n\t}\n\treturn strings.Contains(text, \"not_found_error\") && strings.Contains(text, \"model\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "f66a3aaf9dc87834ca5519d0cae4e1740bd936c1": {"t": 1785748557.5401547, "n": 46021, "h": "dd3f7a5389d4db9a", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tcase \"deepseek\":\n\t\tclient.baseURL = \"https://api.deepseek.com/v1\"\n\tcase \"minimax\":\n\t\tclient.baseURL = \"https://api.minimax.io/anthropic\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "6b6820e30a37425f19d22aad2deb5462d5946dee": {"t": 1785748558.852872, "n": 45883, "h": "5233ceb5f51f5f68", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc looksLikeEnvVarName(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif len(s) < 8 {\n\t\treturn false\n\t}\n\t// Must be all caps/underscores/digits and start with a letter.\n\tfor i, r := range s {\n\t\tif i == 0 {\n\t\t\tif r < 'A' || r > 'Z' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc resolveEnvVarKeyPointer(apiKey string) string {\n\tapiKey = strings.TrimSpace(apiKey)\n\tif apiKey == \"\" {\n\t\treturn \"\"\n\t}\n\tif !looksLikeEnvVarName(apiKey) {\n\t\treturn apiKey\n\t}\n\tif v := strings.TrimSpace(os.Getenv(apiKey)); v != \"\" {\n\t\treturn v\n\t}\n\treturn apiKey\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: resolveEnvVarKeyPointer(apiKey),\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tkeyLen := len(strings.TrimSpace(c.apiKey))\n\tkeyHash := \"\"\n\t{\n\t\tsum := sha256.Sum256([]byte(strings.TrimSpace(c.apiKey)))\n\t\tkeyHash = fmt.Sprintf(\"%x\", sum)[:8]\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d (keyLen=%d keyHash=%s): %s\", resp.StatusCode, keyLen, keyHash, string(body))\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc stripMarkdownCodeFences(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\tlines := strings.Split(s, \"\\n\")\n\tout := make([]string, 0, len(lines))\n\tfor _, ln := range lines {\n\t\tt := strings.TrimSpace(ln)\n\t\tif strings.HasPrefix(t, \"```\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, ln)\n\t}\n\treturn strings.TrimSpace(strings.Join(out, \"\\n\"))\n}\n\n// extractAndCleanJSON extracts the first valid JSON value from an LLM response.\n// It is robust against braces inside JSON strings and markdown code fences.\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\ts := strings.TrimSpace(response)\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\t// Remove markdown code fences, which often introduce leading backticks.\n\ts = stripMarkdownCodeFences(s)\n\n\t// Scan for a JSON object/array start and attempt decoding from there.\n\tfor i := 0; i < len(s); i++ {\n\t\tch := s[i]\n\t\tif ch != '{' && ch != '[' {\n\t\t\tcontinue\n\t\t}\n\t\tdec := json.NewDecoder(strings.NewReader(s[i:]))\n\t\tdec.UseNumber()\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err == nil {\n\t\t\ttrimmed := strings.TrimSpace(string(raw))\n\t\t\tif trimmed != \"\" {\n\t\t\t\treturn trimmed\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(response)\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "1c698372dfe874fce6f1062a950f928961a131c6": {"t": 1785748560.1380398, "n": 44696, "h": "5ecbc2bfd8c177e8", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype anthropicMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype anthropicRequest struct {\n\tModel string `json:\"model\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tTemperature float64 `json:\"temperature,omitempty\"`\n\tMessages []anthropicMessage `json:\"messages\"`\n}\n\ntype anthropicResponse struct {\n\tContent []struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"content\"`\n}\n\ntype anthropicModelsResponse struct {\n\tData []struct {\n\t\tID string `json:\"id\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t} `json:\"data\"`\n}\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif strings.TrimSpace(c.apiKey) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API key not configured\")\n\t}\n\n\tmodel := strings.TrimSpace(profileLLMCall.Model)\n\tif model == \"\" {\n\t\tlatest, lErr := c.getLatestAnthropicModelID(ctx)\n\t\tif lErr != nil {\n\t\t\treturn \"\", lErr\n\t\t}\n\t\tmodel = latest\n\t}\n\n\t// Anthropic API is strict about ASCII in some client setups; keep consistent with other providers.\n\tprompt = sanitizeASCII(prompt)\n\n\treqBody := anthropicRequest{\n\t\tModel: model,\n\t\tMaxTokens: 4000,\n\t\tTemperature: 0.1,\n\t\tMessages: []anthropicMessage{{\n\t\t\tRole: \"user\",\n\t\t\t// Use the content-block format which is compatible with modern Anthropic Messages API.\n\t\t\tContent: []map[string]any{{\"type\": \"text\", \"text\": prompt}},\n\t\t}},\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.baseURL, \"/\")+\"/messages\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\thttpReq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\thttpReq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tfor _, c := range parsed.Content {\n\t\tif strings.TrimSpace(c.Text) != \"\" {\n\t\t\treturn c.Text, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no response content from Anthropic\")\n}\n\nfunc (c *Client) getLatestAnthropicModelID(ctx context.Context) (string, error) {\n\tbase := strings.TrimRight(c.baseURL, \"/\")\n\turl := base + \"/models\"\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create models request: %w\", err)\n\t}\n\treq.Header.Set(\"x-api-key\", strings.TrimSpace(c.apiKey))\n\treq.Header.Set(\"anthropic-version\", \"2023-06-01\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch Anthropic models: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read Anthropic models response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"Anthropic models request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar parsed anthropicModelsResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal Anthropic models response: %w\", err)\n\t}\n\tfor _, m := range parsed.Data {\n\t\tid := strings.TrimSpace(m.ID)\n\t\tif id != \"\" {\n\t\t\t// Docs: newest models are listed first.\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no Anthropic models returned\")\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "369d49bc9b6b2a4ac515139de0400c4542126c59": {"t": 1785748561.4192922, "n": 40812, "h": "d0c3353ef83dc9cc", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Write request body to a temp file to avoid command line length limits\n\tbodyFile, err := os.CreateTemp(\"\", \"bedrock-request-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create body temp file: %w\", err)\n\t}\n\tbodyFilePath := bodyFile.Name()\n\tif _, err := bodyFile.Write(requestBody); err != nil {\n\t\tbodyFile.Close()\n\t\tos.Remove(bodyFilePath)\n\t\treturn \"\", fmt.Errorf(\"failed to write body temp file: %w\", err)\n\t}\n\tbodyFile.Close()\n\tdefer os.Remove(bodyFilePath)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\t// Use fileb:// to read body from file as binary blob to avoid command line length limits\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", \"fileb://\"+bodyFilePath,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "99bb4c07c52bc1df6b67f374052fdec1731610ea": {"t": 1785748562.7000074, "n": 40375, "h": "1bd446ca9dcb4db0", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Create a cross-platform temporary file for the response\n\ttmpFile, err := os.CreateTemp(\"\", \"bedrock-response-*.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\ttmpFilePath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpFilePath)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\ttmpFilePath)\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(tmpFilePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "6a4dd8b0a05cf921701607769b145793fd911708": {"t": 1785748563.988267, "n": 40125, "h": "fa1606002f2b36c8", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "caa5decf9fdac80ec3603bef40536da0b1d9ba95": {"t": 1785748565.2718384, "n": 40111, "h": "53f142a697cc4ee2", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// AskPrompt sends a raw prompt to the configured provider without adding additional wrapper context.\nfunc (c *Client) AskPrompt(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// CleanJSONResponse extracts the first JSON object from a response and applies minimal cleanup.\nfunc (c *Client) CleanJSONResponse(response string) string {\n\treturn c.extractAndCleanJSON(response)\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "cf4301ab6ac058596143849afa18decefbdc8119": {"t": 1785748566.5856588, "n": 39423, "h": "71361e61ea7695d7", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tif len(analysis.Operations) > 0 {\n\t\t\t\t\tfmt.Printf(\"⚠️ Selected %d AWS operation(s) but AWS is not enabled (no AWS client). Re-run with --aws to execute.\\n\", len(analysis.Operations))\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "170630e8ede5b52e5e389905e3b7d5875f2026d2": {"t": 1785748567.8448935, "n": 39235, "h": "153d775f0c62d923", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey == \"\" {\n\t\t\tclient.tryFallbackToOpenAI(fmt.Errorf(\"gemini-api provider configured without API key\"))\n\t\t\tbreak\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\tAPIKey: apiKey,\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t} else {\n\t\t\tclient.tryFallbackToOpenAI(err)\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to OpenAI for best compatibility when no provider specified\n\t\tclient.provider = \"openai\"\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) tryFallbackToOpenAI(reason error) {\n\tfallbackKey := resolveFallbackOpenAIKey(c.apiKey)\n\tif fallbackKey == \"\" {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"Gemini unavailable (%v) and no OpenAI key available for fallback\\n\", reason)\n\t\t}\n\t\treturn\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"Gemini unavailable (%v). Falling back to OpenAI.\\n\", reason)\n\t}\n\n\tc.provider = \"openai\"\n\tc.apiKey = fallbackKey\n\tc.baseURL = \"https://api.openai.com/v1\"\n\tc.geminiClient = nil\n}\n\nfunc resolveFallbackOpenAIKey(existing string) string {\n\tif existing != \"\" {\n\t\treturn existing\n\t}\n\tif key := viper.GetString(\"ai.providers.openai.api_key\"); key != \"\" {\n\t\treturn key\n\t}\n\tif envName := viper.GetString(\"ai.providers.openai.api_key_env\"); envName != \"\" {\n\t\tif envVal := os.Getenv(envName); envVal != \"\" {\n\t\t\treturn envVal\n\t\t}\n\t}\n\tif envVal := os.Getenv(\"OPENAI_API_KEY\"); envVal != \"\" {\n\t\treturn envVal\n\t}\n\treturn \"\"\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "0b03cfd46aca59c96202dcd3e43327f5e19d246b": {"t": 1785748569.158955, "n": 38419, "h": "d0557c0952e72dc2", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/bgdnvk/clanker/internal/agent\"\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the investigative agent\n\tinvestigator := agent.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tinvestigator.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := investigator.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := investigator.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "841116122c944a69535b895442c7d1bb3c6ace3e": {"t": 1785748570.4614484, "n": 38337, "h": "f906f8e12e547cb1", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, err := c.summarizeContextIfNeeded(ctx, question, finalContext.String())\n\tif err != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tconst fallbackLimit = 80000\n\t\tif len(finalContext.String()) > fallbackLimit {\n\t\t\tsummarizedContext = finalContext.String()[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = finalContext.String()\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, summarizedContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Sanitize to ASCII-only to satisfy AWS CLI argv constraints\n\tprompt = sanitizeASCII(prompt)\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode to pass as inline body; ensures ASCII-only\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(sanitizeASCII(prompt), genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: sanitizeASCII(prompt),\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the agent\n\tagent := awsclient.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tagent.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := agent.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := agent.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Summarize context if too large to avoid CLI arg limits and reduce token usage\n\tsummarizedContext, sErr := c.summarizeContextIfNeeded(ctx, question, combinedContext)\n\tif sErr != nil {\n\t\t// Fallback: truncate context if summarization fails\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Summarization failed: %v. Falling back to truncation.\\n\", sErr)\n\t\t}\n\t\tconst fallbackLimit = 80000\n\t\tif len(combinedContext) > fallbackLimit {\n\t\t\tsummarizedContext = combinedContext[:fallbackLimit]\n\t\t} else {\n\t\t\tsummarizedContext = combinedContext\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📦 Combined context length: %d, summarized length: %d\\n\", len(combinedContext), len(summarizedContext))\n\t}\n\n\t// Final LLM call with compacted context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, summarizedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// summarizeContextIfNeeded reduces context size by chunking and summarizing when it exceeds limits\nfunc (c *Client) summarizeContextIfNeeded(ctx context.Context, question, contextText string) (string, error) {\n\t// Allow override via config, else default safe limit under macOS arg max\n\tmaxChars := viper.GetInt(\"ai.max_prompt_chars\")\n\tif maxChars <= 0 {\n\t\tmaxChars = 120000\n\t}\n\n\tif len(contextText) <= maxChars {\n\t\treturn contextText, nil\n\t}\n\n\t// Chunk the context\n\tchunkSize := viper.GetInt(\"ai.chunk_chars\")\n\tif chunkSize <= 0 {\n\t\tchunkSize = 120000\n\t}\n\n\tchunks := chunkString(contextText, chunkSize)\n\t// Limit total chunks to keep latency reasonable\n\tmaxChunks := viper.GetInt(\"ai.max_chunks\")\n\tif maxChunks <= 0 {\n\t\tmaxChunks = 6\n\t}\n\tif len(chunks) > maxChunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🧩 Context split into %d chunks; sampling to %d for summarization...\\n\", len(chunks), maxChunks)\n\t\t}\n\t\tchunks = sampleChunks(chunks, maxChunks)\n\t}\n\tif c.debug {\n\t\tfmt.Printf(\"🧠 Summarizing %d chunk(s), ~%d chars each (target <= %d chars)\\n\", len(chunks), chunkSize, maxChars)\n\t}\n\n\tsummaries := make([]string, 0, len(chunks))\n\tfor i, ch := range chunks {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📝 Summarizing chunk %d/%d (size %d chars)\\n\", i+1, len(chunks), len(ch))\n\t\t}\n\t\tsum, err := c.summarizeChunk(ctx, question, ch, i+1, len(chunks))\n\t\tif err != nil {\n\t\t\t// Fallback: truncate chunk\n\t\t\tconst chunkFallback = 5000\n\t\t\tif len(ch) > chunkFallback {\n\t\t\t\tsummaries = append(summaries, ch[:chunkFallback])\n\t\t\t} else {\n\t\t\t\tsummaries = append(summaries, ch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tsummaries = append(summaries, sum)\n\t}\n\n\t// Merge summaries and, if still too big, do a final pass summarization\n\tmerged := strings.Join(summaries, \"\\n\\n\")\n\tif len(merged) > maxChars {\n\t\tfinal, err := c.summarizeText(ctx, question, merged, \"MERGE\")\n\t\tif err == nil {\n\t\t\t// Ensure within limit\n\t\t\tif len(final) > maxChars {\n\t\t\t\treturn final[:maxChars], nil\n\t\t\t}\n\t\t\treturn final, nil\n\t\t}\n\t\t// Fallback: truncate merged summaries\n\t\treturn merged[:maxChars], nil\n\t}\n\treturn merged, nil\n}\n\n// summarizeChunk summarizes a single chunk with strong guidance\nfunc (c *Client) summarizeChunk(ctx context.Context, question, chunk string, idx, total int) (string, error) {\n\tprompt := fmt.Sprintf(`You are condensing AWS investigation output to the essentials needed to answer the question: \"%s\".\n\nCHUNK %d/%d. Create a concise, lossless summary with only:\n- Specific service/function names, ARNs, and log group names\n- Time ranges, timestamps, counts, metrics\n- Errors/exceptions with messages and frequencies\n- Alarms and states\n- Any anomalies or patterns strongly related to the question\n\nRemove boilerplate, headers, pagination, and duplicates. Keep it under 1500 words.\n\nContent:\\n%s`, question, idx, total, chunk)\n\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// summarizeText performs a final merge summarization\nfunc (c *Client) summarizeText(ctx context.Context, question, text, mode string) (string, error) {\n\tprompt := fmt.Sprintf(`You are merging summarized AWS findings to answer: \"%s\".\n\nTask: Combine the summaries into a single concise context preserving all concrete findings (names, timestamps, errors, counts, states). Remove duplicates. Keep it under 2000 words.\n\nMode: %s\n\nSummaries:\\n%s`, question, mode, text)\n\treturn c.dispatchLLM(ctx, prompt)\n}\n\n// dispatchLLM routes a small prompt to the configured LLM provider\nfunc (c *Client) dispatchLLM(ctx context.Context, prompt string) (string, error) {\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, prompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\n// chunkString splits s into chunks up to size n runes (approx by bytes here)\nfunc chunkString(s string, n int) []string {\n\tif n <= 0 || len(s) <= n {\n\t\treturn []string{s}\n\t}\n\tchunks := make([]string, 0, (len(s)+n-1)/n)\n\tfor start := 0; start < len(s); start += n {\n\t\tend := start + n\n\t\tif end > len(s) {\n\t\t\tend = len(s)\n\t\t}\n\t\tchunks = append(chunks, s[start:end])\n\t}\n\treturn chunks\n}\n\n// sampleChunks selects up to k chunks evenly from the sequence, preserving start/end\nfunc sampleChunks(chunks []string, k int) []string {\n\tif k <= 0 || len(chunks) <= k {\n\t\treturn chunks\n\t}\n\tsampled := make([]string, 0, k)\n\t// Always include first and last\n\tsampled = append(sampled, chunks[0])\n\tsampled = append(sampled, chunks[len(chunks)-1])\n\tif k == 2 {\n\t\treturn sampled\n\t}\n\t// Evenly sample remaining from the middle range\n\tremaining := k - 2\n\tstep := float64(len(chunks)-2) / float64(remaining+1)\n\tused := map[int]bool{0: true, len(chunks) - 1: true}\n\tfor i := 1; i <= remaining; i++ {\n\t\tidx := 1 + int(step*float64(i))\n\t\tif idx >= len(chunks)-1 {\n\t\t\tidx = len(chunks) - 2\n\t\t}\n\t\tif !used[idx] {\n\t\t\tsampled = append(sampled, chunks[idx])\n\t\t\tused[idx] = true\n\t\t}\n\t}\n\treturn sampled\n}\n\n// sanitizeASCII strips non-ASCII runes to avoid CLI argv issues and provider limits\nfunc sanitizeASCII(s string) string {\n\t// Fast path: if all bytes < 128\n\tallASCII := true\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] >= 128 {\n\t\t\tallASCII = false\n\t\t\tbreak\n\t\t}\n\t}\n\tif allASCII {\n\t\treturn s\n\t}\n\tb := make([]byte, 0, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] < 128 {\n\t\t\tb = append(b, s[i])\n\t\t}\n\t}\n\treturn string(b)\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "ee76f4829dab8effee360e0bacbff5a445e4e4fa": {"t": 1785748571.7198582, "n": 32759, "h": "a30218e4fe1510af", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, finalContext.String())\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode the request body for AWS CLI\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(prompt, genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the agent\n\tagent := awsclient.NewAgent(c.awsClient, c.debug)\n\n\t// Set AI decision function so agent can make intelligent decisions\n\tagent.SetAIDecisionFunction(func(ctx context.Context, prompt string) (string, error) {\n\t\treturn c.Ask(ctx, prompt, \"\", \"\")\n\t})\n\n\tagentContext, err := agent.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := agent.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Final LLM call with comprehensive context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n- THINK DEEPLY and analyze every piece of data provided\n- EXAMINE ALL EVIDENCE carefully and look for patterns, anomalies, and connections\n- REASON THROUGH the implications of each finding systematically\n- CONSIDER multiple perspectives and potential root causes\n- SYNTHESIZE information across different services and data sources\n- Include ALL specific details, data points, and findings from the investigation\n- Quote exact messages, outputs, and critical information when available \n- Highlight any important issues, warnings, errors, or notable patterns discovered\n- Provide specific names, values, timestamps, and concrete data points\n- Do not summarize away important details - show them when relevant\n- Focus on actionable findings with concrete evidence from the gathered data\n- Present information clearly with proper context and explanations\n- THINK STEP BY STEP through your analysis and reasoning\n- CHALLENGE your initial assumptions and verify conclusions with evidence\n- PRIORITIZE findings by severity and business impact\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, combinedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "3ab3f0606ad7b25c1ca958995591137405e9f93d": {"t": 1785748573.1844697, "n": 32560, "h": "19c0dfb5c854fde9", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// Check if this query would benefit from intelligent agent investigation\n\tif c.shouldUseAgent(question) && c.awsClient != nil {\n\t\treturn c.askWithAgentInvestigation(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Otherwise use the standard dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, finalContext.String())\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode the request body for AWS CLI\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(prompt, genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n\n// shouldUseAgent determines if the query would benefit from intelligent agent investigation\nfunc (c *Client) shouldUseAgent(question string) bool {\n\tquestionLower := strings.ToLower(question)\n\n\t// Agent keywords that indicate need for log analysis and service investigation\n\tagentKeywords := []string{\n\t\t\"chat\", \"logs\", \"latest\", \"recent\", \"error\", \"issue\", \"problem\", \"failure\",\n\t\t\"debug\", \"investigate\", \"analyze\", \"status\", \"health\", \"performance\",\n\t\t\"image\", \"processing\", \"service\", \"api\", \"response\", \"timeout\",\n\t\t\"summary\", \"what happened\", \"whats wrong\", \"why\", \"how\", \"when\",\n\t}\n\n\tfor _, keyword := range agentKeywords {\n\t\tif strings.Contains(questionLower, keyword) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// askWithAgentInvestigation uses the intelligent agent to gather context before answering\nfunc (c *Client) askWithAgentInvestigation(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🤖 Using intelligent agent for context investigation...\\n\")\n\t}\n\n\t// Find the appropriate AI profile for agent operations\n\tprofile := c.findAgentProfile(profileInfraAnalysis)\n\tif profile == nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ No suitable AI profile found for agent operations, falling back to standard approach\\n\")\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Create and run the agent\n\tagent := awsclient.NewAgent(c.awsClient, c.debug)\n\tagentContext, err := agent.InvestigateQuery(ctx, question)\n\tif err != nil {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"⚠️ Agent investigation failed: %v, falling back to standard approach\\n\", err)\n\t\t}\n\t\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n\t}\n\n\t// Build final context with agent's findings\n\tfinalContext := agent.BuildFinalContext(agentContext)\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Agent gathered %d chars of context in %d steps\\n\", len(finalContext), agentContext.CurrentStep)\n\t}\n\n\t// Combine with existing contexts\n\tcombinedContext := \"\"\n\tif awsContext != \"\" {\n\t\tcombinedContext += \"=== EXISTING AWS CONTEXT ===\\n\" + awsContext + \"\\n\\n\"\n\t}\n\tif codeContext != \"\" {\n\t\tcombinedContext += \"=== CODE CONTEXT ===\\n\" + codeContext + \"\\n\\n\"\n\t}\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tcombinedContext += \"=== GITHUB CONTEXT ===\\n\" + githubContext[0] + \"\\n\\n\"\n\t}\n\n\tcombinedContext += finalContext\n\n\t// Final LLM call with comprehensive context\n\tfinalPrompt := fmt.Sprintf(`Based on the comprehensive investigation below, please answer the user's question: \"%s\"\n\n%s\n\nCRITICAL INSTRUCTIONS:\n- THINK DEEPLY and analyze every piece of data provided\n- EXAMINE ALL EVIDENCE carefully and look for patterns, anomalies, and connections\n- REASON THROUGH the implications of each finding systematically\n- CONSIDER multiple perspectives and potential root causes\n- SYNTHESIZE information across different services and data sources\n- Include ALL specific details, data points, and findings from the investigation\n- Quote exact messages, outputs, and critical information when available \n- Highlight any important issues, warnings, errors, or notable patterns discovered\n- Provide specific names, values, timestamps, and concrete data points\n- Do not summarize away important details - show them when relevant\n- Focus on actionable findings with concrete evidence from the gathered data\n- Present information clearly with proper context and explanations\n- THINK STEP BY STEP through your analysis and reasoning\n- CHALLENGE your initial assumptions and verify conclusions with evidence\n- PRIORITIZE findings by severity and business impact\n\nTake your time to thoroughly analyze the data. Think extremely hard about what the evidence tells you and what actions should be taken. Please provide a comprehensive, actionable response based on the gathered information, ensuring all critical findings and specific details are prominently featured.`, question, combinedContext)\n\n\t// Use the same AI provider for the final response\n\tvar response string\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\tresponse, err = c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\tresponse, err = c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tresponse, err = c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\tresponse, err = c.askBedrock(ctx, finalPrompt)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get final AI response: %w\", err)\n\t}\n\n\treturn response, nil\n}\n\n// findAgentProfile finds the appropriate AI profile for agent operations\nfunc (c *Client) findAgentProfile(fallbackProfile string) *awsclient.AIProfile {\n\t// Try to find agent-specific profile first\n\tif c.awsClient != nil {\n\t\tprofiles := c.awsClient.GetAIProfiles()\n\n\t\t// Look for agent-specific profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"agent\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Look for LLM call profiles\n\t\tfor name, profile := range profiles {\n\t\t\tif strings.Contains(strings.ToLower(name), \"llm-call\") {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the fallback profile if specified\n\t\tif fallbackProfile != \"\" {\n\t\t\tif profile, exists := profiles[fallbackProfile]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use the default AI provider from config\n\t\tdefaultProvider := viper.GetString(\"ai.default_provider\")\n\t\tif defaultProvider != \"\" {\n\t\t\tif profile, exists := profiles[defaultProvider]; exists {\n\t\t\t\treturn &profile\n\t\t\t}\n\t\t}\n\n\t\t// Use default profile as last resort\n\t\tif profile, exists := profiles[\"default\"]; exists {\n\t\t\treturn &profile\n\t\t}\n\n\t\t// If we have any profiles, use the first one\n\t\tfor _, profile := range profiles {\n\t\t\treturn &profile\n\t\t}\n\t}\n\n\treturn nil\n}\n"}, "a87f7847d3ae6fbe26220787850f785b643acbc0": {"t": 1785748574.4175143, "n": 26437, "h": "5cbd7f85a50f88ca", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// All providers use the dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, finalContext.String())\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t\tfmt.Printf(\"📤 Query to LLM: %s\\n\", question)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode the request body for AWS CLI\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(prompt, genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n"}, "de3f784bebb00e38c5b7aeba1c9349b1fd21674b": {"t": 1785748575.690895, "n": 26390, "h": "f69eacd58ebfd8b4", "c": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t// AWS SDK imports - commented out but kept for future use\n\t// \"github.com/aws/aws-sdk-go-v2/aws\"\n\t// \"github.com/aws/aws-sdk-go-v2/config\"\n\t// \"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\tawsclient \"github.com/bgdnvk/clanker/internal/aws\"\n\tghclient \"github.com/bgdnvk/clanker/internal/github\"\n\t\"google.golang.org/genai\"\n)\n\ntype Client struct {\n\tprovider string\n\tapiKey string\n\tbaseURL string\n\tgeminiClient *genai.Client\n\tawsClient *awsclient.Client\n\tgithubClient *ghclient.Client\n\taiProfile string\n\tdebug bool\n\n\t// AWS SDK fields - commented out but kept for future use\n\t// bedrockClient *bedrockruntime.Client\n\t// awsConfig aws.Config\n}\n\n// Tool definitions for function calling\ntype Tool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tInputSchema ToolInputSchema `json:\"input_schema\"`\n}\n\ntype ToolInputSchema struct {\n\tType string `json:\"type\"`\n\tProperties map[string]string `json:\"properties\"`\n\tRequired []string `json:\"required\"`\n}\n\n// Bedrock Claude request/response types\ntype ClaudeRequest struct {\n\tAnthropicVersion string `json:\"anthropic_version\"`\n\tMaxTokens int `json:\"max_tokens\"`\n\tMessages []Message `json:\"messages\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tContent []ClaudeContent `json:\"content\"`\n\tID string `json:\"id\"`\n\tModel string `json:\"model\"`\n\tRole string `json:\"role\"`\n\tType string `json:\"type\"`\n}\n\ntype ClaudeContent struct {\n\tText string `json:\"text\"`\n\tType string `json:\"type\"`\n}\n\n// OpenAI types (keeping for compatibility)\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype OpenAIResponse struct {\n\tChoices []Choice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tMessage Message `json:\"message\"`\n}\n\nfunc NewClient(provider, apiKey string, debug bool, aiProfile ...string) *Client {\n\tclient := &Client{\n\t\tprovider: provider,\n\t\tapiKey: apiKey,\n\t\tdebug: debug,\n\t}\n\n\t// Set AI profile if provided, otherwise find the first profile-llm-call* profile\n\tif len(aiProfile) > 0 && aiProfile[0] != \"\" {\n\t\tclient.aiProfile = aiProfile[0]\n\t} else {\n\t\tclient.aiProfile = client.findLLMCallProfile()\n\t}\n\n\tswitch provider {\n\tcase \"bedrock\", \"claude\":\n\t\t// AWS SDK initialization - commented out but kept for future use\n\t\t// This was working before but had SSO credential caching issues\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(),\n\t\t// config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\n\t\t// Currently using AWS CLI approach which works reliably with SSO\n\tcase \"gemini\":\n\t\t// For Gemini, use Application Default Credentials (like gemini CLI)\n\t\t// User should run: gcloud auth application-default login\n\t\tctx := context.Background()\n\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t// No APIKey - will automatically use Application Default Credentials\n\t\t\t// This works just like the gemini CLI tool\n\t\t})\n\t\tif err == nil {\n\t\t\tclient.geminiClient = geminiClient\n\t\t}\n\tcase \"gemini-api\":\n\t\t// For Gemini API (requires API key from Google AI Studio)\n\t\tif apiKey != \"\" {\n\t\t\tctx := context.Background()\n\t\t\tgeminiClient, err := genai.NewClient(ctx, &genai.ClientConfig{\n\t\t\t\tAPIKey: apiKey,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tclient.geminiClient = geminiClient\n\t\t\t}\n\t\t}\n\tcase \"openai\":\n\t\tclient.baseURL = \"https://api.openai.com/v1\"\n\tcase \"anthropic\":\n\t\tclient.baseURL = \"https://api.anthropic.com/v1\"\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI, but AWS SDK was working before)\n\t\t// AWS SDK approach:\n\t\t// cfg, err := config.LoadDefaultConfig(context.Background(), config.WithSharedConfigProfile(\"commercial-dev\"))\n\t\t// if err == nil {\n\t\t// client.provider = \"bedrock\"\n\t\t// client.awsConfig = cfg\n\t\t// client.bedrockClient = bedrockruntime.NewFromConfig(cfg)\n\t\t// }\n\t\tclient.provider = \"bedrock\"\n\t}\n\n\treturn client\n}\n\nfunc NewClientWithTools(provider, apiKey string, awsClient *awsclient.Client, githubClient *ghclient.Client, debug bool, aiProfile ...string) *Client {\n\tclient := NewClient(provider, apiKey, debug, aiProfile...)\n\tclient.awsClient = awsClient\n\tclient.githubClient = githubClient\n\treturn client\n}\n\nfunc (c *Client) GetTools() []Tool {\n\treturn []Tool{\n\t\t{\n\t\t\tName: \"get_latest_batch_jobs\",\n\t\t\tDescription: \"Get information about the latest AWS Batch jobs\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"limit\": \"Number of latest jobs to retrieve (default: 5)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_lambda_functions\",\n\t\t\tDescription: \"Get information about AWS Lambda functions\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for function names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ec2_instances\",\n\t\t\tDescription: \"Get information about EC2 instances\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"state\": \"Optional state filter (running, stopped, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_clusters\",\n\t\t\tDescription: \"Get information about ECS clusters and services\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name filter\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_ecs_tasks\",\n\t\t\tDescription: \"Get information about ECS tasks\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"cluster\": \"Optional cluster name to filter tasks\",\n\t\t\t\t\t\"service\": \"Optional service name to filter tasks\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_cloudwatch_logs\",\n\t\t\tDescription: \"Get CloudWatch log groups information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"filter\": \"Optional filter for log group names\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"get_github_workflows\",\n\t\t\tDescription: \"Get GitHub Actions workflow information\",\n\t\t\tInputSchema: ToolInputSchema{\n\t\t\t\tType: \"object\",\n\t\t\t\tProperties: map[string]string{\n\t\t\t\t\t\"status\": \"Optional status filter (active, disabled, etc.)\",\n\t\t\t\t},\n\t\t\t\tRequired: []string{},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (c *Client) Ask(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// Use the default profile from the AI configuration for LLM calls\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use the AWS profile from the AI configuration as the infrastructure analysis profile\n\t// This maintains backward compatibility\n\treturn c.AskWithTools(ctx, question, awsContext, codeContext, profileLLMCall.AWSProfile, githubContext...)\n}\n\n// AskWithTools performs the full AWS tool calling workflow\nfunc (c *Client) AskWithTools(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\t// All providers use the dynamic three-stage approach\n\treturn c.askWithDynamicAnalysis(ctx, question, awsContext, codeContext, profileInfraAnalysis, githubContext...)\n}\n\n// askWithDynamicAnalysis implements the three-stage dynamic analysis approach for all AI providers\nfunc (c *Client) askWithDynamicAnalysis(ctx context.Context, question, awsContext, codeContext, profileInfraAnalysis string, githubContext ...string) (string, error) {\n\tif c.debug {\n\t\tfmt.Printf(\"🔍 Stage 1: Analyzing query with dynamic tool selection...\\n\")\n\t}\n\n\t// Stage 1: Ask Claude to analyze the query and determine what AWS operations are needed\n\t// This uses the LLM profile (profileLLMCall) from the AI configuration\n\tanalysisPrompt := awsclient.GetLLMAnalysisPrompt(question)\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Analysis prompt length: %d characters\\n\", len(analysisPrompt))\n\t}\n\n\t// Get analysis from the configured AI provider (uses AI profile for LLM calls)\n\tvar analysisResponse string\n\tvar err error\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\tcase \"openai\":\n\t\tanalysisResponse, err = c.askOpenAI(ctx, analysisPrompt)\n\tcase \"anthropic\":\n\t\tanalysisResponse, err = c.askAnthropic(ctx, analysisPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\tanalysisResponse, err = c.askGemini(ctx, analysisPrompt)\n\tdefault:\n\t\tanalysisResponse, err = c.askBedrock(ctx, analysisPrompt)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to analyze query: %w\", err)\n\t}\n\n\tif c.debug {\n\t\tfmt.Printf(\"📥 Raw analysis response length: %d characters\\n\", len(analysisResponse))\n\t\tfmt.Printf(\"📄 Raw analysis response: %s\\n\", analysisResponse)\n\t}\n\n\t// Clean and extract JSON from the response\n\tcleanedResponse := c.extractAndCleanJSON(analysisResponse)\n\tif c.debug {\n\t\tfmt.Printf(\"🧹 Cleaned JSON length: %d characters\\n\", len(cleanedResponse))\n\t\tfmt.Printf(\"🔧 Cleaned JSON: %s\\n\", cleanedResponse)\n\t}\n\n\t// Parse the analysis response\n\tvar analysis awsclient.LLMAnalysis\n\tif err := json.Unmarshal([]byte(cleanedResponse), &analysis); err != nil {\n\t\t// If JSON parsing fails, fall back to original approach\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"❌ Warning: Failed to parse analysis response, falling back to no-tool approach: %v\\n\", err)\n\t\t\tfmt.Printf(\"📋 Raw response (first 500 chars): %s...\\n\", func() string {\n\t\t\t\tif len(cleanedResponse) > 500 {\n\t\t\t\t\treturn cleanedResponse[:500]\n\t\t\t\t}\n\t\t\t\treturn cleanedResponse\n\t\t\t}())\n\t\t}\n\t\t// Continue with no AWS operations\n\t} else {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"✅ Successfully parsed analysis: %d operations found\\n\", len(analysis.Operations))\n\t\t\tfor i, op := range analysis.Operations {\n\t\t\t\tfmt.Printf(\" %d. %s - %s\\n\", i+1, op.Operation, op.Reason)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 2: Execute the determined AWS operations concurrently using AWS client\n\tif c.debug {\n\t\tfmt.Printf(\"🔧 Stage 2: Executing AWS operations...\\n\")\n\t}\n\tvar awsResults string\n\tif c.awsClient != nil && len(analysis.Operations) > 0 {\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🚀 Executing %d operations with infrastructure profile: %s...\\n\", len(analysis.Operations), profileInfraAnalysis)\n\t\t}\n\n\t\t// Get the region for the infrastructure analysis profile from config\n\t\tregion := c.getRegionForAWSProfile(profileInfraAnalysis)\n\n\t\tvar err error\n\t\tawsResults, err = c.awsClient.ExecuteOperationsWithAWSProfile(ctx, analysis.Operations, profileInfraAnalysis, region)\n\t\tif err != nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"❌ Warning: Failed to execute AWS operations: %v\\n\", err)\n\t\t\t}\n\t\t\tawsResults = \"\"\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"✅ AWS operations completed. Results length: %d characters\\n\", len(awsResults))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif c.awsClient == nil {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"⚠️ AWS client is nil - cannot execute operations\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tif c.debug {\n\t\t\t\tfmt.Printf(\"ℹ️ No operations to execute\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Stage 3: Build final context and get Claude's response\n\tif c.debug {\n\t\tfmt.Printf(\"📝 Stage 3: Building final context and getting response...\\n\")\n\t}\n\tvar finalContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tfinalContext.WriteString(\"Initial AWS Context:\\n\")\n\t\tfinalContext.WriteString(awsContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"📊 Added initial AWS context: %d characters\\n\", len(awsContext))\n\t\t}\n\t}\n\n\tif awsResults != \"\" {\n\t\tfinalContext.WriteString(\"Live AWS Data:\\n\")\n\t\tfinalContext.WriteString(awsResults)\n\t\tfinalContext.WriteString(\"\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🔥 Added live AWS data: %d characters\\n\", len(awsResults))\n\t\t}\n\t}\n\n\tif codeContext != \"\" {\n\t\tfinalContext.WriteString(\"Code Context:\\n\")\n\t\tfinalContext.WriteString(codeContext)\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"💻 Added code context: %d characters\\n\", len(codeContext))\n\t\t}\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tfinalContext.WriteString(\"GitHub Context:\\n\")\n\t\tfinalContext.WriteString(githubContext[0])\n\t\tfinalContext.WriteString(\"\\n\\n\")\n\t\tif c.debug {\n\t\t\tfmt.Printf(\"🐙 Added GitHub context: %d characters\\n\", len(githubContext[0]))\n\t\t}\n\t}\n\n\t// Build final prompt\n\tfinalPrompt := fmt.Sprintf(`%s\n\nContext:\n%s\n\nPlease provide a comprehensive answer based on the live data above.`, question, finalContext.String())\n\n\tif c.debug {\n\t\tfmt.Printf(\"🎯 Final prompt length: %d characters\\n\", len(finalPrompt))\n\t\tfmt.Printf(\"🚀 Sending final request to AI provider (%s)...\\n\", c.provider)\n\t}\n\n\t// Use the same provider switching logic as in the analysis phase\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, finalPrompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, finalPrompt)\n\tcase \"gemini\", \"gemini-api\":\n\t\treturn c.askGemini(ctx, finalPrompt)\n\tdefault:\n\t\treturn c.askBedrock(ctx, finalPrompt)\n\t}\n}\n\n// Original Ask method for backward compatibility - replaced above\nfunc (c *Client) AskOriginal(ctx context.Context, question, awsContext, codeContext string, githubContext ...string) (string, error) {\n\t// If we have AWS/GitHub clients, try to get specific data based on the question\n\tvar enhancedContext strings.Builder\n\n\tif awsContext != \"\" {\n\t\tenhancedContext.WriteString(awsContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tenhancedContext.WriteString(codeContext)\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\tif len(githubContext) > 0 && githubContext[0] != \"\" {\n\t\tenhancedContext.WriteString(githubContext[0])\n\t\tenhancedContext.WriteString(\"\\n\")\n\t}\n\n\t// Analyze question and get specific data if we have the right clients\n\tif c.awsClient != nil {\n\t\tspecificData, err := c.getSpecificAWSData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific AWS Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tif c.githubClient != nil {\n\t\tspecificData, err := c.getSpecificGitHubData(ctx, question)\n\t\tif err == nil && specificData != \"\" {\n\t\t\tenhancedContext.WriteString(\"Specific GitHub Data:\\n\")\n\t\t\tenhancedContext.WriteString(specificData)\n\t\t\tenhancedContext.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Build the prompt with enhanced context\n\tprompt := c.buildPrompt(question, enhancedContext.String(), \"\", \"\")\n\n\tswitch c.provider {\n\tcase \"bedrock\", \"claude\":\n\t\treturn c.askBedrock(ctx, prompt)\n\tcase \"gemini\":\n\t\treturn c.askGemini(ctx, prompt)\n\tcase \"anthropic\":\n\t\treturn c.askAnthropic(ctx, prompt)\n\tcase \"openai\":\n\t\treturn c.askOpenAI(ctx, prompt)\n\tdefault:\n\t\t// Default to Bedrock (currently uses AWS CLI)\n\t\t// AWS SDK fallback was: if c.bedrockClient != nil { return c.askBedrock(ctx, prompt) }\n\t\treturn c.askBedrock(ctx, prompt)\n\t}\n}\n\nfunc (c *Client) buildPrompt(question, awsContext, codeContext, githubContext string) string {\n\tvar prompt strings.Builder\n\n\tprompt.WriteString(\"You are an AI assistant helping with AWS infrastructure, codebase analysis, and GitHub repository management. \")\n\tprompt.WriteString(\"Answer the following question based on the provided context.\\n\\n\")\n\n\tif awsContext != \"\" {\n\t\tprompt.WriteString(\"AWS Infrastructure Context:\\n\")\n\t\tprompt.WriteString(awsContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif codeContext != \"\" {\n\t\tprompt.WriteString(\"Codebase Context:\\n\")\n\t\tprompt.WriteString(codeContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tif githubContext != \"\" {\n\t\tprompt.WriteString(\"GitHub Repository Context:\\n\")\n\t\tprompt.WriteString(githubContext)\n\t\tprompt.WriteString(\"\\n\")\n\t}\n\n\tprompt.WriteString(\"Question: \")\n\tprompt.WriteString(question)\n\tprompt.WriteString(\"\\n\\nPlease provide a helpful and accurate answer based on the context above.\")\n\n\treturn prompt.String()\n}\n\nfunc (c *Client) askBedrock(ctx context.Context, prompt string) (string, error) {\n\t// NOTE: Using AWS CLI approach due to SSO credential caching issues with Go SDK\n\t// Previous working AWS SDK approach (commented out for reference):\n\t// cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(\"commercial-dev\"))\n\t// bedrockClient := bedrockruntime.NewFromConfig(cfg)\n\t// result, err := bedrockClient.InvokeModel(ctx, input)\n\n\t// Get AI profile configuration (this is the profileLLMCall for Bedrock API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Use AWS CLI directly since it works while Go SDK has SSO credential issues\n\trequest := ClaudeRequest{\n\t\tAnthropicVersion: \"bedrock-2023-05-31\",\n\t\tMaxTokens: 4000,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\trequestBody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\t// Base64 encode the request body for AWS CLI\n\tencodedBody := base64.StdEncoding.EncodeToString(requestBody)\n\n\t// Call AWS CLI with LLM profile from config (for Bedrock API access)\n\tcmd := exec.CommandContext(ctx, \"aws\", \"bedrock-runtime\", \"invoke-model\",\n\t\t\"--model-id\", profileLLMCall.Model,\n\t\t\"--body\", encodedBody,\n\t\t\"--profile\", profileLLMCall.AWSProfile,\n\t\t\"--region\", profileLLMCall.Region,\n\t\t\"/tmp/bedrock-response.json\")\n\n\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"AWS_PROFILE=%s\", profileLLMCall.AWSProfile))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"AWS CLI call failed: %w, output: %s\", err, string(output))\n\t}\n\n\t// Read the response file\n\tresponseData, err := os.ReadFile(\"/tmp/bedrock-response.json\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response file: %w\", err)\n\t}\n\n\tvar response ClaudeResponse\n\tif err := json.Unmarshal(responseData, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Content) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response content from Claude\")\n\t}\n\n\treturn response.Content[0].Text, nil\n}\n\n// executeAWSTool executes a specific AWS tool with the given parameters\nfunc (c *Client) askGemini(ctx context.Context, prompt string) (string, error) {\n\tif c.geminiClient == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini client not initialized\")\n\t}\n\n\t// Get the AI profile configuration (this is the profileLLMCall for Gemini API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\t// Create content from text\n\tcontent := genai.NewContentFromText(prompt, genai.RoleUser)\n\n\t// Generate content using the configured model\n\tresp, err := c.geminiClient.Models.GenerateContent(ctx, profileLLMCall.Model, []*genai.Content{content}, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate content with Gemini: %w\", err)\n\t}\n\n\tif len(resp.Candidates) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response candidates from Gemini\")\n\t}\n\n\t// Extract text from the first candidate\n\tvar result strings.Builder\n\tfor _, part := range resp.Candidates[0].Content.Parts {\n\t\tif part.Text != \"\" {\n\t\t\tresult.WriteString(part.Text)\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) askOpenAI(ctx context.Context, prompt string) (string, error) {\n\t// Get the AI profile configuration (this is the profileLLMCall for OpenAI API access)\n\tprofileLLMCall, err := c.getAIProfile(c.aiProfile)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get AI profile for LLM calls: %w\", err)\n\t}\n\n\tif c.apiKey == \"\" {\n\t\treturn \"\", fmt.Errorf(\"OpenAI API key not configured\")\n\t}\n\n\trequest := OpenAIRequest{\n\t\tModel: profileLLMCall.Model,\n\t\tMessages: []Message{\n\t\t\t{\n\t\t\t\tRole: \"user\",\n\t\t\t\tContent: prompt,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.baseURL+\"/chat/completions\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar response OpenAIResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(response.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no response from AI\")\n\t}\n\n\treturn response.Choices[0].Message.Content, nil\n}\n\nfunc (c *Client) askAnthropic(ctx context.Context, prompt string) (string, error) {\n\t// Direct Anthropic API implementation would go here\n\treturn \"Direct Anthropic API integration not implemented. Use Bedrock instead.\", nil\n}\n\nfunc (c *Client) getSpecificAWSData(ctx context.Context, question string) (string, error) {\n\tif c.awsClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for batch job queries\n\tif strings.Contains(questionLower, \"batch\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"batch jobs latest\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Latest AWS Batch Jobs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for Lambda function queries\n\tif strings.Contains(questionLower, \"lambda\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"lambda functions status\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Lambda Functions Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for EC2 instance queries\n\tif strings.Contains(questionLower, \"ec2\") && (strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"status\") || strings.Contains(questionLower, \"latest\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ec2 instances running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Running EC2 Instances:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for CloudWatch logs queries\n\tif strings.Contains(questionLower, \"log\") && (strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"error\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"cloudwatch logs recent\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent CloudWatch Logs:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS cluster queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"cluster\") || strings.Contains(questionLower, \"service\") || strings.Contains(questionLower, \"status\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs clusters services\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Clusters and Services:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Check for ECS task queries\n\tif strings.Contains(questionLower, \"ecs\") && (strings.Contains(questionLower, \"task\") || strings.Contains(questionLower, \"running\") || strings.Contains(questionLower, \"container\")) {\n\t\tinfo, err := c.awsClient.GetRelevantContext(ctx, \"ecs tasks running\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"ECS Tasks Status:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\nfunc (c *Client) getSpecificGitHubData(ctx context.Context, question string) (string, error) {\n\tif c.githubClient == nil {\n\t\treturn \"\", nil\n\t}\n\n\tquestionLower := strings.ToLower(question)\n\tvar result strings.Builder\n\n\t// Check for workflow queries\n\tif strings.Contains(questionLower, \"workflow\") || strings.Contains(questionLower, \"action\") || strings.Contains(questionLower, \"ci\") {\n\t\tif strings.Contains(questionLower, \"latest\") || strings.Contains(questionLower, \"recent\") || strings.Contains(questionLower, \"status\") {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflow runs latest\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"Latest Workflow Runs:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"workflows\")\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(\"GitHub Workflows:\\n\")\n\t\t\t\tresult.WriteString(info)\n\t\t\t\tresult.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for PR queries\n\tif strings.Contains(questionLower, \"pr\") || strings.Contains(questionLower, \"pull\") && strings.Contains(questionLower, \"request\") {\n\t\tinfo, err := c.githubClient.GetRelevantContext(ctx, \"pull requests\")\n\t\tif err == nil {\n\t\t\tresult.WriteString(\"Recent Pull Requests:\\n\")\n\t\t\tresult.WriteString(info)\n\t\t\tresult.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\treturn result.String(), nil\n}\n\n// extractAndCleanJSON extracts and cleans JSON from Claude's response\nfunc (c *Client) extractAndCleanJSON(response string) string {\n\t// Find the JSON object in the response\n\tstart := strings.Index(response, \"{\")\n\tif start == -1 {\n\t\treturn response // Return original if no JSON found\n\t}\n\n\t// Find the matching closing brace\n\tbraceCount := 0\n\tend := -1\n\nfindEnd:\n\tfor i := start; i < len(response); i++ {\n\t\tswitch response[i] {\n\t\tcase '{':\n\t\t\tbraceCount++\n\t\tcase '}':\n\t\t\tbraceCount--\n\t\t\tif braceCount == 0 {\n\t\t\t\tend = i + 1\n\t\t\t\tbreak findEnd\n\t\t\t}\n\t\t}\n\t}\n\n\tif end == -1 {\n\t\treturn response // Return original if no complete JSON found\n\t}\n\n\t// Extract the JSON portion\n\tjsonStr := response[start:end]\n\n\t// Clean common problematic characters that break JSON parsing\n\t// Replace backticks with regular quotes in JSON strings\n\tjsonStr = strings.ReplaceAll(jsonStr, \"`\", \"'\")\n\n\treturn jsonStr\n}\n"}}}}